Integrate music streaming from Funkwhale & Navidrome (Subsonic)

Name: Mohammad Amanullah
Email: ullahaman072003@gmail.com
Matrix Handle: @m.amanullah7:matrix.org
IRC Nickname: @m.amanullah7
LinkedIn: https://www.linkedin.com/in/mananullah7/
LeetCode: https://leetcode.com/u/m_amanullah7/
Mentor: Lucifer/mayhem
Timezone: Indian Standard Time (GMT +5:30)
Languages: English, Hindi

I’m Mohammad Amanullah, currently a pre final year student at National Institute of Technology Agartala and along with that i’m a diploma level student at Indian Institute of Technology Madras. My coding journey started in jan 2023 just few month after joining college, my journey started with c college course , then i decided to shift c++ as i was interested
in competitive programming(cp), and slowly i started enjoying doing cp, in second year python course introduced and i loved python other than any other language, and after that a new course and project introduced “MAD1” (modern application development) in which my techstack was Flask, jinja2, bootstrap,js this how my development journey started and in this course project i scored 100/100 S grade.

Other than my academic i’m leading WeCan a social club at NIT Agartala, student led non profit organisation working toward the empowerment of underprivileged children through education

When i’m neither coding and nor in weCan then i must be playing Cricket, Football or listening songs on loudspeaker, sometimes i also play BGMI and i’m not big fan of movies until unless its science fiction

CV: AmanullahxCv.pdf


Why Me?

The strongest thing about me is my problem solving skills as a competitive programmer. I get intuition very fast and even if I don’t have knowledge or its the first time I’m seeing it then I learn very fast as my never give up attitude never lets me give up.

In the past few months i’m familiarized with listenbrainz codebase, and trying to contribute as much as i can. In this period i’ve interacted with mentors in community and they helped me to throughout the time and i learned a lot.

My Pull Request

PR #3233: 404 Error Fix in explore.py: Improved error handling for the “Music Neighborhood” page.

The root cause was that the fetchone() method returned None when the database query didn’t find any matching records, likely because the artist name or ID I was searching for didn’t exist in the database


Proposed Project

Project length: 350 hours

Allow users to play music from their Funkwhale servers as well as Navidrome directly in BrainzPlayer, both are self hoisted music streaming platform. Funkwhale used a OAuth2 for secure and safe authentication, but currently Navidrome used basic subsonic authentication (username/password + salt), but soon OAuth2 authentication also will be available for Navidrome.

NOTE: if OAuth2 available then will implement it instead of basic authentication (username/password + salt)

Funkwhale (175 hours):

  • Backend:

    • Funkwhaleservice class that will handle Funkwhale API calls and funkwhale _servers table which will save server details as well as OAuth2 tokens.
  • Frontend:

    • Will create a React and TypeScript form in the Connect Services section for users to input their Funkwhale server url.
  • BrainzPlayer Integration:

    • For this I need to create a FunkwhalePlayer class to BrainzPlayer so users can search and stream tracks, and we also need to take care of proper state management.

Here is the detailed Flow Chart: Refer Here

Implementation

Db Schema: funkwhale _servers table to store each user funkwhale server url and
OAuth2 credentials.

CREATE TABLE funkwhale_servers (
    user_id         INTEGER NOT NULL,
    host_url        TEXT NOT NULL,
    client_id       TEXT,
    client_secret   TEXT,
    access_token    TEXT,
    refresh_token   TEXT,
    token_expiry    INTEGER,
    PRIMARY KEY (user_id, host_url)
);

Will add this table via a migration and I need to create a function in db/funkwhale.py
which will save and retrieve server details. I’ll also add validation to check for duplicate
entries and ensure the host_url is a valid URL format before saving.


I’ll define a funkwhaleServer model in listenbrainz/db/model/funkwhale.py to map the table schema:

listenbrainz/db/models/funkwhale.py

from sqlalchemy import Column, Integer, Text, PrimaryKeyConstraint
from listenbrainz.db import Base

class FunkwhaleServer(Base):
    __tablename__ = 'funkwhale_servers'

    user_id = Column(Integer, nullable=False)
    host_url = Column(Text, nullable=False)
    client_id = Column(Text)
    client_secret = Column(Text)
    access_token = Column(Text)
    refresh_token = Column(Text)
    token_expiry = Column(Integer)

    __table_args__ = (
        PrimaryKeyConstraint('user_id', 'host_url', name='pk_funkwhale_servers'),
    )

Now will create funkwhaleService class which will handle both API calls as well as OAuth2 authentication

Class added: listenbrainz/domain/funkwhale_services.py

import requests
import time
from listenbrainz.db import funkwhale as db_funkwhale

class FunkwhaleService:
    def __init__(self, user_id: int):
        self.user_id = user_id
        self.servers = db_funkwhale.get_funkwhale_servers(user_id)
        if not self.servers:
            raise ValueError("No Funkwhale servers configured for user")
        self.server = self.servers[0]
        self.base_url = self.server.host_url
        self.client_id = self.server.client_id
        self.client_secret = self.server.client_secret
        self.access_token = self.server.access_token
        self.refresh_token = self.server.refresh_token
        self.token_expiry = self.server.token_expiry

    def create_funkwhale_app(self) -> tuple[str, str]:
        url = f"{self.base_url}/api/v1/oauth/apps"
        data = {
            "client_name": "ListenBrainz",
            "scopes": "read:library",
            "redirect_uris": "https://listenbrainz.org/callback/funkwhale"
        }
        response = requests.post(url, json=data, timeout=5)
        response.raise_for_status()
        app_data = response.json()
        return app_data["client_id"], app_data["client_secret"]

    def get_authorization_url(self) -> str:
        """
        Generate OAuth2 authorization URL.
        """
        auth_reqst = {
            "client_id": self.client_id,
            "response_type": "code",
            "redirect_uri": "https://listenbrainz.org/callback/funkwhale",
            "scope": "read:library"
        }
        return f"{self.base_url}/oauth/authorize?{urlencode(auth_reqst)}"

    def exchange_code_for_token(self, code: str) -> dict:
        """
        Exchange of authorization code to access/refresh tokens.
        """
        url = f"{self.base_url}/api/v1/oauth/token"
        data = {
            "grant_type": "authorization_code",
            "code": code,
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "redirect_uri": "https://listenbrainz.org/callback/funkwhale"
        }
        response = requests.post(url, data=data, timeout=5)
        return response.json()

    def search(self, title: str, artist: str) -> list:
        """
        Search for tracks using Funkwhale API.
        """
        if self.token_expiry and self.token_expiry <= int(time.time()):
            token_data = self.refresh_access_token()
            self.access_token = token_data["access_token"]
            self.refresh_token = token_data.get("refresh_token", self.refresh_token)
            self.token_expiry = int(time.time()) + token_data["expires_in"]
            db_funkwhale.save_funkwhale_server(self.user_id, self.base_url, self.client_id, self.client_secret,
                                              self.access_token, self.refresh_token, self.token_expiry)
        url = f"{self.base_url}/api/v1/tracks"
        headers = {"Authorization": f"Bearer {self.access_token}"}
        params = {"q": f"{title} {artist}"}
        response = requests.get(url, headers=headers, params=params, timeout=5)
        return response.json().get("results", [])

I’ll also add an API endpoint in webserver/views/api.py to handle funkwhale server configuration:

listenbrainz/webserver/views/api.py

from flask import Blueprint, request, jsonify, redirect
from listenbrainz.domain.funkwhale_services import FunkwhaleService
from listenbrainz.db import funkwhale as db_funkwhale

api_bp = Blueprint('api', __name__)

@api_bp.route('/funkwhale-config', methods=['POST'])
def funkwhale_config():
    data = request.get_json()
    user_id = data.get('user_id')
    host_url = data.get('host_url')

    if not all([user_id, host_url]):
        return jsonify({'error': 'Missing required fields'}), 400

    servers = db_funkwhale.get_funkwhale_servers(user_id)
    server = next((s for s in servers if s.host_url == host_url), None)

    if not server:
        db_funkwhale.save_funkwhale_server(user_id, host_url)
        service = FunkwhaleService(user_id)
        client_id, client_secret = service.create_funkwhale_app()
        db_funkwhale.save_funkwhale_server(user_id, host_url, client_id, client_secret)
    else:
        service = FunkwhaleService(user_id)

    auth_url = service.get_authorization_url()
    return jsonify({'redirect_url': auth_url}), 200

@api_bp.route('/callback/funkwhale', methods=['GET'])
def funkwhale_callback():
    """
    Handled the Funkwhale OAuth2 callback.
    """
    code = request.args.get('code')
    user_id = request.args.get('user_id')
    if not code or not user_id:
        return jsonify({'error': 'Missing code or user_id'}), 400
    service = FunkwhaleService(int(user_id))
    token_data = service.exchange_code_for_token(code)
    db_funkwhale.save_funkwhale_server(user_id, service.base_url, service.client_id, service.client_secret,
                                      token_data["access_token"], token_data["refresh_token"],
                                      int(time.time()) + token_data["expires_in"])
    return redirect("https://listenbrainz.org/settings/music-services/details/?success=funkwhale")

Frontend: Will create a React form in the Connect Services section for users to input their funkwhale host_url details. The form will include fields for host URL a Connect to flunkwhale button to connect, and a Disable button to disconnect. After submission, it will display a success or error message. As discussed with @lucifer I’ll embed this form directly into MusicServices.tsx as a new section, matching the styling of existing sections (e.g., SoundCloud, Apple Music) with a white background, orange underline for the heading, and green/gray buttons.

Mockup

This is how we will send a POST request to the backend to initiate the Funkwhale OAuth2 flow. It will be similar to Apple Music, Spotify, and SoundCloud. As discussed with @lucifer, Funkwhale supports OAuth2, so we are directly embedding a form to send a POST request from the Connect Services page, which will be user-friendly. Users can connect directly from the Connect Services page instead of navigating to a new page.

Note: This is a basic mockup and can be changed according to reviews. It will follow the same styling as Spotify, SoundCloud, etc.

Class added in `MusicServices.tsx:

frontend/js/src/settings/music-service/details/MusicServices.tsx

import React, { useState } from 'react';
import './MusicServices.css';

const FunkwhaleSettings: React.FC = () => {
    const [hostUrl, setHostUrl] = useState<string>('');
    const [message, setMessage] = useState<string>('');
    const [isConnected, setIsConnected] = useState<boolean>(false);
    const userId = 1; // Placeholder; will fetch from user context

    const handleConnect = async () => {
        try {
            const response = await fetch('/api/funkwhale-config', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    user_id: userId,
                    host_url: hostUrl,
                }),
            });
            const data = await response.json();
            if (!response.ok) {
                throw new Error(data.error || 'Failed to connect');
            }
            window.location.href = data.redirect_url; // Redirect to Funkwhale OAuth2 authorization
        } catch (error) {
            setMessage(error.message);
            setIsConnected(false);
        }
    };

    const handleDisable = () => {
        setIsConnected(false)
        setMessage('Funkwhale connection disabled')
        // Logic for the removal of  server details from the database (will be implemented)
    };

    return (
        <div className="service-section">
            <h2>Funkwhale</h2>
            <p>Connect to your Funkwhale server to play music on ListenBrainz.</p>
            <div className="form-group">
                <label htmlFor="host-url">Host URL</label>
                <input
                    type="text"
                    id="host-url"
                    value={hostUrl}
                    onChange={(e) => setHostUrl(e.target.value)}
                    placeholder="e.g., https://my-funkwhale.server"
                />
            </div>
            <div className="button-group">
                <button className="connect-btn" onClick={handleConnect} disabled={isConnected}>
                    <span className="checkmark">{isConnected ? '✔' : ''}</span>
                    Play music on ListenBrainz
                </button>
                <button className="disable-btn" onClick={handleDisable} disabled={!isConnected}>
                    Disable
                </button>
                <p className="button-message">
                    {isConnected
                        ? 'Connect to your Funkwhale server to play music using Funkwhale on ListenBrainz.'
                        : 'You will not be able to listen to music on ListenBrainz using Funkwhale.'}
                </p>
            </div>
            {message && <p className={isConnected ? 'success-message' : 'error-message'}>{message}</p>}
        </div>
    );
};

// To include FunkwhaleSettings in MusicServices.tsx
const MusicServices: React.FC = () => {
    return (
        <div className="music-services">
            <FunkwhaleSettings />
        </div>
    );
};

export default MusicServices;

BrainzPlayer Integration: For this I need to create a funkwhalePlayer class to BrainzPlayer so users can search and stream tracks, and we also need to take care of proper state management. I’ll add the funkwhalePlayer class to the existing BrainzPlayer.tsx without creating a new page.

Class added in BrainzPlayer.tsx:
frontend/js/src/brainzplayer/BrainzPlayer.tsx

import React from 'react';
import { FunkwhaleService } from '../../domain/funkwhale_services'

interface Track {
    id: string;
    title: string;
    artist: string;
    duration: number;
    streamUrl: string;
}

interface BrainzPlayerState {
    currentTrack: Track | null;
    seekPosition: number;
    progress: number;
    isPlaying: boolean;
}

class FunkwhalePlayer {
    private funkwhaleService: FunkwhaleService;

    constructor(funkwhaleService: FunkwhaleService) {
        this.funkwhaleService = funkwhaleService;
    }

    async searchTrack(title: string, artist: string): Promise<Track[]> {
        const tracks = await this.funkwhaleService.search(title, artist);
        return tracks.map((track: any) => ({
            id: track.id,
            title: track.title,
            artist: track.artist?.name || "Unknown Artist",
            duration: track.duration || 0,
            streamUrl: ''
        }));
    }

    // Pseudocode for remaining methods
    async streamTrack(trackId: string): Promise<string> {
        """
        Get stream URL for a track using Funkwhale API.
        """
        streamUrl = await self.funkwhaleService.stream(trackId)
        return streamUrl
    }
}

class BrainzPlayer extends React.Component<{}, BrainzPlayerState> {
    private funkwhalePlayer: FunkwhalePlayer;
    private audioElement: HTMLAudioElement | null = null;

    constructor(props: {}) {
        super(props);
        this.state = {
            currentTrack: null,
            seekPosition: 0,
            progress: 0,
            isPlaying: false
        };
        const funkwhaleService = new FunkwhaleService(1); // Placeholder user_id
        this.funkwhalePlayer = new FunkwhalePlayer(funkwhaleService);
    }

    async playTrack(title: string, artist: string) {
        const tracks = await this.funkwhalePlayer.searchTrack(title, artist);
        if (tracks.length === 0) {
            console.error('Track not found');
            return;
        }

        const track = tracks[0];
        const streamUrl = await this.funkwhalePlayer.streamTrack(track.id);
        track.streamUrl = streamUrl;

        this.setState({
            currentTrack: track,
            seekPosition: 0,
            progress: 0,
            isPlaying: true
        });

        if (this.audioElement) {
            this.audioElement.src = streamUrl;
            this.audioElement.play();
        }
    }

    handleTimeUpdate = () => {
        """
        Update the playback progress and submit the  listens after 40-50% of its duration.
        """
        if self.audioElement and self.state.currentTrack:
            currentTime = self.audioElement.currentTime
            duration = self.state.currentTrack.duration
            progress = (currentTime / duration) * 100
            self.setState({
                seekPosition: currentTime,
                progress: progress
            })
            if currentTime >= 200 or progress >= 40:
                self.submitListen()
    };

    submitListen = async () => {
        """
        Submit listen to /1/submit-listens endpoint.
        """
        // Logic to submit the listen (to be implemented)
    };

    render() {
        return (
            <div className="brainz-player">
                <audio
                    ref={(el) => (this.audioElement = el)}
                    onTimeUpdate={this.handleTimeUpdate}
                />
                {/* UI elements to track the info, progress bar, play/pause buttons */}
            </div>
        );
    }
}

export default BrainzPlayer;

Navidrome (175 hours):

Allow users to play music from their Navidrome servers directly in BrainzPlayer. There are mainly 3 core functionalities which I need to solve:

  • Backend: NavidromeService class that will handle Navidrome API calls and a navidrome_servers table which will save user credentials.

  • Frontend: Will create a React form in the Connect Services section for users to input their Navidrome server details.

  • BrainzPlayer Integration: For this I need to create a NavidromePlayer class to BrainzPlayer so users can search and stream tracks, and we also need to take care of proper state management.

Here is the detailed Flow Chart: Refer Here:


Db Schema: navidrome_servers table to store Navidrome credentials, which should support multiple servers per user.

Will add this table via a migration and I need to create a function in db/navidrome.py which will save and retrieve server details. I’ll also add validation to check for duplicate entries and ensure the host_url is a valid URL format before saving.

I’ll define a NavidromeServer model in listenbrainz/db/model/navidrome.py to map the table schema

I’ll also add an API endpoint in webserver/views/api.py to handle Navidrome server configuration:

Mockup


Frontend

Will create a React form in the Connect Services section for users to input their Navidrome server details. The form will include fields for host URL, username, and password, a Play music on ListenBrainz button to connect, and a Disable button to disconnect. After submission, it will display a success or error message. As discussed with @lucifer I’ll embed this form directly into MusicServices.tsx as a new section, matching the styling of existing sections (e.g, SoundCloud, Apple Music) with a white background, orange underline for the heading, and green/gray buttons.

This is how we will directly send a POST request to the backend to validate the Navidrome server. It will be similar as Apple Music, Spotify, Sound Cloud. As discussed with @lucifer Navidrome doesn’t support OAuth2 if it’s by the timeline will implement the OAuth2 else we will continuing with basic authentication. we are directly embedding a form to send a post request directly from connected service page which will be user friendly to use them and they can directly from connect service page instead of navigating to a new page.

Note: Max Word limit exceeded for Navindrome Integration you can check my proposal pdf:
Integrate music streaming from Funkwhale & Navidrome


Timeline (12 Weeks, 350 Hours) - Funkwhale and Navidrome

Phase 1: Funkwhale

  • Week 1: Community Bonding, Deep research, local env setup.

    • Go throughout the Funkwhale API, explore and research.
    • For testing set up a local Funkwhale server.
    • Decide and finalise the implementation and finalise research on Funkwhale API and
      OAuth2 flow.
    • Start the initial project setup (e.g, environment configuration, dependencies, etc.).
  • Week 2-3: Backend

    • Will add funkwhale_servers table to store server URLs and OAuth2 tokens.
    • Will add FunkwhaleService class in listenbrainz/domain/funkwhale_services.py which will
      handle API calls and also the OAuth2 authentication.
    • Will add /api/funkwhale-config endpoint which will initiate the OAuth2 flow.
    • Cross check and validate API endpoint functionality with the local Funkwhale server.
  • Week 4-5: Frontend

    • Refine the mockup and create frontend same as mockup components using React and
      TypeScript.
    • Will add the Funkwhale UI into MusicServices.tsx (form for server URL, connect/disconnect
      buttons).
    • Integrate the backend with frontend through /api/funkwhale-config which will initiate
      OAuth2 flow.
    • Testing of the OAuth2 redirect and callback flow.
  • Week 6-7: FunkwhalePlayer

    • Will add FunkwhalePlayer class in BrainzPlayer.tsx.
    • Will add search and streaming features using Funkwhale API.
    • Validation of components to check state effectively (e.g., track progress, play/pause).
    • Verify that the listens are submitted accurately and correctly to /1/submit-listens after 40- 50% of track duration.

Phase 2: Navidrome

  • Week 8-9: Buffer & Deep Research & Local Env Setup

    • Fix Bugs, improvement, resolve issues (Funkwhale).
    • Deep research, collect more information about the Navidrome and Subsonic API.
    • Navidrome local environment setup for testing.
    • Decide the research on Subsonic API and authentication.
    • Check for Navidrome’s latest OAuth2 authentication available or not.
    • Use if OAuth2 available for Navidrome’s authentication else use basic authentication
      (username/password + salt).
  • Week 10: Backend

    • Fix Bugs, improvement, resolve issues (Funkwhale).
    • Will add navidrome_servers table which will store server URLs, usernames, and passwords.
    • Will integrate the NavidromeService class in listenbrainz/domain/navidrome_services.py
      which will handle Subsonic API calls also handle authentication.
    • Will add /api/navidrome-config endpoint which will validate server details.
    • Using a local Navidrome server validate the API endpoint functionality.
  • Week 11: Frontend

    • Fix Bugs, improvement, resolve issues (Funkwhale).
    • Refine mockup and create frontend same as mockup components using React and
      TypeScript.
    • Will add the Navidrome UI into MusicServices.tsx (form for server URL, username,
      password, connect/disconnect buttons).
    • Integrate the backend with frontend through /api/navidrome-config to validate server details.
    • Will add NavidromePlayer class in BrainzPlayer.tsx for searching and streaming.
    • Validation of components to check state effectively (e.g., track progress, play/pause).
  • Week 12: Testing & Final Adjustments

    • Fix Bugs, improvement, resolve issues both Funkwhale and Navidrome.
    • Documentation for both Funkwhale and Navidrome.
    • Review code coverage for both Funkwhale and Navidrome.
    • Address feedback for both Funkwhale and Navidrome.

Community Affinities

What type of music do you listen to? (please list a series of [MBIDs](MusicBrainz Identifier - MusicBrainz) as examples)

I mostly love to listen song like pop, rock and sad and love Songs.

  • Ek Din Teri Raahon ( MBID: 2dd0fb05-b7ca-4301-8d1a-ebdc9ca2673a
  • Excuses (MBID: 5c1dcd12-605c-4717-bc19-095162fba01e
  • Animals (MBID: 25704ed9-c86f-4f3d-991a-d293830697a0)

What aspects of MusicBrainz/ListenBrainz/BookBrainz/Picard interest you the most?

I love how ListenBrainz helps me track all my listening at one place and provide a platform to stream music from all over the application at one place. It’s open source which I also like the most .

Have you ever used MusicBrainz Picard to tag your files or used any of our projects in the past?

No as of now i didn’t use


Programming Precedents

When did you first start programming?
My coding journey started in jan 2023 just few month after joining college, my journey started with c college course , then i decided to shift c++ as i was interested in competitive programming(cp)

Have you contributed to other open source projects? If so, which projects and can we see some of your code?
No listenBrainz is my first open source in which i have contributed

If you have not contributed to open source projects, do you have other code we can look at?

I’ve created various project but most and also working on some project my recent project
In which i got perfect 100/100 s grade

HousehelpHub Github
HousehelpHub Link

What sorts of programming projects have you done on your own time?

I’ve done various different types of projects over different periods of time like HousehelpHub, Fitometer, sehatGPT, EventEase, Rback,etc these projects gives me edge of working on different languages and can learn new tech very fast

For more detail in you can refer my github: mAmanullah7

Practical Requirements

I currently use a M1 Macbook Air, 8GB RAM, 512GB SSD.

How much time are you available weekly, and how would you plan to use it?

In summer break, I can easily work 40 hours a week for the project as I won’t have any other commitments.

2 Likes

Hi,

GSoC proposals should be under the GSoC applications community; please move this post there if you would like it to be considered for GSoC.

Thanks @julian45 for pointing out it’s updated now!

Thanks @chaban for moving my proposal to Gsoc Section really appreciate it!

Hi!

Thanks for the proposal.

We discussed this internally and I think you should focus on just integrating funkwhale, a subsonic implementation. But it supports OAuth instead of subsonic’s token + salt/password implementation as it is much more secure. Further, as you intend to do a 175 hours project. I think focussing on integration of just one service is the right scope.

You can look into existing spotify/apple music/soundcloud code to see how to integrate services using OAuth2. It should be somewhat similar except that anyone can run their own funkwhale server so you will still need the form you have mentioned.

Here are some pointers to help you understand and re-draft the proposal:

  1. A form to let the user specify the Funkwhale server’s URL
  2. The backend uses the submitted url to create a Funkwhale app (if one already doesn’t exist for that server), using Funkwhale’s api/v1/oauth/apps endpoint.
  3. After the app has been created, its the regular OAuth2 flow.

You can find more details about Funkwhale’s OAuth2 implementation at Swagger UI and API authentication — funkwhale 1.4.0 documentation.

1 Like

Thanks for the review @lucifer!!
Surely i will updated the proposal as per your review.

Should i also create a new mockup present mockup is enough just i’ll remove the username password form and also i need to modify my flowchart?

I have 80 days summer vacation starting from may to august! i can dedicate time, what if i wants to do this project 350hrs? what additional features i need to integrate/implement!?

@lucifer I’ve updated my proposal as per review, if there any improvements do let me know!

1 Like