About me
Name: Rana Satyaraj
Matrix Handle: Satyaraj
IRC Nickname: satyaraj
Location: Roorkee, Uttarakhand
Time Zone: UTC +05:30
e-mail: ranasatyaraj7103iitr@gmail.com
Github: SatyarajRana (Rana Satyaraj) · GitHub
Linkedin: https://www.linkedin.com/in/satyaraj-rana-751471230/
I’m Satyaraj Rana, currently a Junior at the Indian Institute of Technology Roorkee, Uttarakhand. I have been teaching myself coding for the past 2.5 years and have gained experience in Full Stack Web Development. When I’m not coding, I’m either playing Minecraft or playing some sport like Cricket, Table Tennis or Football. Also I’m deeply passionate about Cinema, my favorite one being Interstellar.
Why me?
My love for problem-solving. I think I’m a very resourceful guy. If you give me a problem, more than my skills, I trust my resourcefulness that I will come up with a solution. Additionally in my past two internships, I have gained substantial experience both in Development and in Communication and Teamwork.
In the past 2 months, I’ve familiarized with the Listenbrainz codebase, and although I’ve faced issues with setting up the environment on my laptop due to some zsh issues, with guidance from Monkey and Lucifer, I’ve been able to make some contributions to the project.
Here are my PRs:
https://github.com/metabrainz/listenbrainz-server/pull/2805
https://github.com/metabrainz/listenbrainz-server/pull/2765
https://github.com/metabrainz/listenbrainz-server/pull/2818
I am currently working on:
https://tickets.metabrainz.org/browse/LB-841
Proposed Project
Listenbrainz currently supports Spotify, Soundcloud and YouTube as the music discovery and streaming services. It would be great if the users were able to search and stream music on LB from their private music collection using apps like Subsonic, Navidrome, Funkwhale and other music apps that support the Subsonic API.
Project size: 350 Hours
Goals:
-
Storing the HOST address of the service as well as user’s credentials by creating a Subsonic player service in the backend.
-
Adding the react based frontend which allows the user to setup/edit their Subsonic service by providing the Host URL and credentials.
-
Adding a new data-source: SubsonicPlayer to the BrainzPlayer and adding all the functionalities such as searching and playing tracks.
Implementation:
First, we have to handle the request when a user first tries to connect Subsonic as a service. For Subsonic service setup, we can have a form that takes the Username, Password and HostURL. This form will be in a new card at Connect Services. The UI for it should be fairly straightforward.
On submission of this form, the
‘music-services/<servce_name>/disconnect/’ route in the backend will be called with the Username, Password and HostURL in the request data. There are two different ways to handle this further:
- Call the add_new_user function inside the music_services_disconnect function itself.
- Redirect the user to to a checkSubsonic page with the credentials which will check the connection by pinging the Subsonic server, then call the ‘music-services/<service_name>/callback’ route and further call the add_new_user function
Either way, the add_new_user function must be defined.
A new class called SubsonicService has to be created which inherits from the ExternalService class, and add it to the ExternalServiceType enum, and accordingly update the getservice_or_raise_404 in settings.py.
class SubsonicService(ExternalService):
def __init__(self):
super(SubsonicService, self).__init__(ExternalServiceType.SUBSONIC)
def add_new_user(self, user_id: int, username: str, password: str, hostUrl: str) -> bool:
# Hashing the password using Subsonic_Key from config
subsonic_key = current_app.config["SUBSONIC_KEY"]
password_with_key = password + subsonic_key
hashed_password = hashlib.md5(password_with_key.encode()).hexdigest()
external_service_oauth.save_info(db_conn, user_id, self.service_type, username, hashed_password, hostUrl)
return True
Additionally the following functions will also be added:
- get_user()
- revoke_user()
- update_user()
In order to handle the database operations to store the username, password_hash and hostUrl, the following functions will be added either to the external_service_oauth.py file or in a new subsonic.py file in the db folder.
1 save_info
2 update_info
The save_info function can be defined as:
def save_info(db_conn, user_id: int, service: ExternalServiceType, username: str, password: str,
host_url: str, record_listens: bool, scopes: List[str], external_user_id: Optional[str] = None):
""" Add a row to the external_service_oauth table for specified user with corresponding info.
Args:
db_conn: database connection
user_id: the ListenBrainz row ID of the user
service: the service for which the info will be saved
username: the username for the service
password: the password for the service
host_url: the URL of the host
record_listens: True if user wishes to import listens, False otherwise
scopes: the oauth scopes
external_user_id: the user's id in the external linked service
"""
result = db_conn.execute(sqlalchemy.text("""
INSERT INTO external_service_oauth AS eso
(user_id, external_user_id, service, username, password, host_url, scopes)
VALUES
(:user_id, :external_user_id, :service, :username, :password, :host_url, :scopes)
ON CONFLICT (user_id, service)
DO UPDATE SET
external_user_id = EXCLUDED.external_user_id,
username = EXCLUDED.username,
password = EXCLUDED.password,
host_url = EXCLUDED.host_url,
scopes = EXCLUDED.scopes,
last_updated = NOW()
RETURNING id
"""), {
"user_id": user_id,
"external_user_id": external_user_id,
"service": service.value,
"username": username,
"password": password,
"host_url": host_url,
"scopes": scopes,
})
if record_listens:
external_service_oauth_id = result.fetchone().id
db_conn.execute(sqlalchemy.text("""
INSERT INTO listens_importer
(external_service_oauth_id, user_id, service)
VALUES
(:external_service_oauth_id, :user_id, :service)
ON CONFLICT (user_id, service) DO UPDATE SET
external_service_oauth_id = EXCLUDED.external_service_oauth_id,
user_id = EXCLUDED.user_id,
service = EXCLUDED.service,
last_updated = NULL,
latest_listened_at = NULL,
error_message = NULL
"""), {
"external_service_oauth_id": external_service_oauth_id,
"user_id": user_id,
"service": service.value
})
db_conn.commit()
This also requires changes to be made in the ExternalService model. The updated model will look like this:
class ExternalService(db.Model):
_tablename_ = 'external_service_oauth'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
# Workaround: cannot use Enum here because it seems SQLAlchemy uses the variable names of the enum instead of
# the values assigned to them. It is possible to write a wrapper to change this behaviour but for our purposes
# just using a string works fine so not going into that.
service = db.Column(db.String, nullable=False)
access_token = db.Column(db.String) #nullable changed to true
refresh_token = db.Column(db.String)
token_expires = db.Column(db.DateTime(timezone=True))
last_updated = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
scopes = db.Column(db.ARRAY(db.String))
user = db.relationship('User')
username = db.Column(db.String, nullable=True). #New column for Subsonic
password = db.Column(db.String, nullable=True). #New column for Subsonic
hostUrl = db.Column(db.String, nullable=True). #New column for Subsonic
About the API:
The subsonic API calls require a Username, the MD5 hash of password, the key used to create the hash, and the server address.
Some of the methods we will be calling are as follows:
Note: Here,
fixed_parameters = u=&t=<password_hash>&s=&v=1.13.0&c=myapp&f=json
base_url = http://your-server/rest
-
Search:
base_url/search2.view?fixed_parameters&query=${encodeURLComponent()} -
Ping:
base_url/ping.view?fixed_parameters -
Stream:
base_url/search2/.view?fixed_parameters&id=
Frontend:
In the frontend, we need to create a new SubsonicPlayer class which implements DataSourceType.
The functions of this class will require the subsonic credentials username, hostUrl and password hash. These will be stored as private variables in the class and will come from the prop: subsonicUser. This prop will be passed down by the BrainzPlayer, which will get it from the GlobalAppContext.
The global props are set in the get_global_props function in utils.py. We will add a new function get_current_subsonic_user function here which will be defined in view_utils.py. This function will call the get_user function inside the SubsonicService class in the backend.
Following are the main functions defined inside of the SubsonicPlayer class.
- playListen: This function will have a helper function searchAndPlayTrack which will call a new searchForSubsonicTrack function defined in utils.tsx. This function calls looks like this:
const searchForSubsonicTrack = async(
subsonic_server_url: string,
subsonic_username: string,
subsonic_password: string,
susbonic_key: string,
trackName: string,
): Promise<string | null> => {
const response = await fetch(
`${subsonic_server_url}/rest/search2.view?u=${subsonic_username}&t=${subsonic_password}&s=${susbonic_key}&v=1.13.
0&c=listenbrainz&f=json&query=${encodeURIComponent(trackName)}`
);
const responseBody = await response.json();
if (!response.ok) {
throw responseBody.error;
}
return responseBody?["subsonic-response"].searchResult2.song.id ?? null;
}
This function returns an id (of the song). The stream method will be called with this id, which will return an audio (blob) object that will be stored in a react state with the type HTMLAudioElement.
searchAndPlayTrack = async (listen: Listen | JSPFTrack): Promise<void> => {
const trackname = getTrackName(listen);
const {
handleError,
handleWarning,
handleSuccess,
onTrackNotFound,
} = this.props;
if (!trackName && !artistName && !releaseName) {
handleWarning(
"We are missing a track title, artist or album name to search on Spotify",
"Not enough info to search on Spotify"
);
onTrackNotFound();
return;
}
try {
const track_id = searchForSubsonicTrack(
this.subsonicUrl,
this.subsonicUsername,
this.subsonicPassword,
this.subsonicKey,
trackName
);
if (track_id) {
this.playSubsonicId(track_id);
return;
}
onTrackNotFound();
}catch (errorObject) {
if (!has(errorObject, "status")) {
handleError(
errorObject.message ?? errorObject,
"Error searching on Spotify"
);
}
if (errorObject.status === 401) {
// Handle token error and try again if fixed
this.handleTokenError(
errorObject.message,
this.searchAndPlayTrack.bind(this, listen)
);
return;
}
if (errorObject.status === 403) {
this.handleAccountError();
}
}
}
playSubsonicId = async(
track_id: string,
retryCount = 0
): Promise<void> => {
const {handleErorr} = this.props;
if (retryCount > 3) {
handleError("Cannot connect to the Subsonic Server");
return;
}
if(!pingSubsonicServer){
this.playSubsonicId.bind(this, track_id, retrCount+1);
}
try{
const response = await fetch(
`${this.subsonicUrl}/rest/stream.view?u=${this.subsonicUsername}&t=${this.subsonicPassword}&
s=${this.subsonicKey}&v=1.13.0&c=listenbrainz&f=json&id=${track_id}`
);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
setAudio(new Audio(URL));
audio?.play();
}catch (error){
handleError(error.message, "Error playing from Subsonic");
}
}
The audio react state can be defined as follows:
const [audio, setAudio] = React.useState<HTMLAudioElement | null>(null);
The play, pause, and currentTime methods can be called on this audio state to control the playback.
● seekToPosition: Seeking to a specific point in the song can be easily implemented using the currentTime property of the HTMLAudioElement.
● onTrackUpdated: The progress bar of the BrainzPlayer can be updated by adding an event listener timeupdate event of the HTMLAudioElement.
audio.addEventListener("timeupdate", () =>
handleTimeUpdate(audio.currentTime)
);
● onTrackEnded: The ending of a track can be detected by adding the ended event-listener to the audio element.
audio.addEventListener("ended", handleTrackEnded);
Project Timeline:
Pre-Community Boding period (April) :
I plan to discuss additional features like Importing listens with Lucifer and Mayhem, which for now, Lucifer has told me to exclude from the proposal. That would enable us to implement features like playlist imports as well.
Community Bonding Period (May) :
Create and finalize the UI MockUps for the Subsonic Service setup page. Also finalize the changes to be made in the external service schema with Lucifer.
Week 1:
In the backend, make the required changes in the external_service_oauth table schema. Also, update the code at places this change in schema affects.
Week 2:
Create the SubsonicService class, and define all its functions including the DB-related functions. i.e. save_info, update_info, etc. Also update the routes in settings.py that will call these functions.
Week 3-4:
Finalize the UI for the Subsonic server setup as well as code it in the frontend.
Week 5:
Create the new SubsonicPlayer component in the frontend and integrate it with the BrainzPlayer.
Week 6-7:
Define all the functions related to music playback in the SubsonicPlayer.
Week 8:
Write tests for the Subsonic class in the backend as well as the DB functions.
Week 9:
Write tests for the SubsonicPlayer in the frontend. Also I will discuss about secondary goals and additional functionality that need to be added.
Week 10-11:
This time will be dedicated to work on all the additional features apart from music playback that might be discussed. Also tidying up the code.
Week 12:
Buffer period, submitting my code for review.
I will keep the community updated about all the work on Monday meetings, as well as discuss the possibility of additional features, like listens Importing and any playlist related functionalities.
Community Affinities
What type of music do you listen to?
I have a wide variety of playlists in my Spotify account. Everything from EDM, Pop and Rock to Classical, Studio Ghibli, Lofi. I listen to a lot of Marshmello, Khalid and Coldplay songs.
Here are some of my favorite songs of all time:
Silence - Marshmello & Khalid : bb1f17e0-bdea-4816-9a2d-4819ad98f894
See You Again - Charlie Puth : 7b66c815-30fc-434e-b530-25e12f0b21c4
Father and Son - Cat Stevens : cf2d532a-ef95-4319-8315-835d355de249
Bohemian Rhapsody - Queen : b1a9c0e9-d987-4042-ae91-78d6a3267d69
Here is my ListenBrainz profile
What aspects of MusicBrainz/ListenBrainz/BookBrainz/Picard interest you the most?
I think ListenBrainz some really cool features like similar users and stats. It makes music discovery much better and also helps track all my music. Also, I’m loving the new Music-Neighbourhood feature!
Have you ever used any of our projects in the past?
I’ve been using ListenBrainz for a few months now.
Programming Precedents
When did I first start programming?
I started programming in my 1st year of college. I taught myself C and C++ initially. Also learned the fundamentals of Data Structures and Algorithms. Then I got into Web Development, initially with the MERN stack. Later on, I made myself familiar with Python, SQL, GraphQL, Socket io, etc.
Have you contributed to other open-source projects? If so, which projects and can we see some of your code?
ListenBrainz is the first Open Source project I’ve contributed to and I think It’s a fantastic start to kick-off my open source journey.
Some of the projects that I have worked on are:
Practical Requirements
I currently use a M1 Macbook Air, 8GB RAM, 256GB SSD.
How much time are you available weekly, and how would you plan to use it?
In the summer break, I can easily dedicate 30-40 hours a week for the project as I won’t have any other engagements.