diff --git a/ListenBrainz.py b/ListenBrainz.py new file mode 100644 index 0000000..119d205 --- /dev/null +++ b/ListenBrainz.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass + +@dataclass +class ListenBrainzScrobble: + artist_name: str + release_name: str + track_name: str + + # Optional + artist_mbids: list[str] | None = None + release_group_mbid: str | None = None + release_mbid: str | None = None + recording_mbid: str | None = None + track_mbid: str | None = None + work_mbids: list[str] | None = None + tracknumber: str | None = None + isrc: str | None = None + spotify_id: str | None = None + tags: list[str] | None = None + media_player: str | None = None + media_player_version: str | None = None + submission_client: str | None = None + submission_client_version: str | None = None + music_service: str | None = None + music_service_name: str | None = None + origin_url: str | None = None + duration: int | None = None + duration_ms: int | None = None + + @staticmethod + def from_json(data: dict): + additional_info_keys = [ + 'artist_mbids', + 'release_group_mbid', + 'release_mbid', + 'recording_mbid', + 'track_mbid', + 'work_mbids', + 'tracknumber', + 'isrc', + 'spotify_id', + 'tags', + 'media_player', + 'media_player_version', + 'submission_client', + 'submission_client_version', + 'music_service', + 'music_service_name', + 'origin_url', + 'duration', + 'duration_ms' + ] + + add_info = data['additional_info'] + return ListenBrainzScrobble( + artist_name=data['artist_name'], + release_name=data['release_name'], + track_name=data['track_name'], + **{key:(add_info[key] if key in add_info else None) for key in additional_info_keys} + ) \ No newline at end of file diff --git a/MusicDatabase.py b/MusicDatabase.py new file mode 100644 index 0000000..a380dc0 --- /dev/null +++ b/MusicDatabase.py @@ -0,0 +1,162 @@ +import datetime +import sqlite3 +import logging +import os + +from dataclasses import dataclass +from typing import Self + +DATABASE = "music-history.db" + +@dataclass +class Album: + id: int + name: str + mbid: str | None = None + cover_art_url: str | None = None + + @classmethod + def from_row(cls, row: tuple[int, str, str | None, str | None]) -> Self: + return cls(*row) + +class Artist: + pass + +@dataclass +class Song: + id: int + name: str + length: int + album_id: int + artist_ids: list[int] + mbid: str | None = None # Music Brainz Id + + @classmethod + def from_rows(cls, rows: list[tuple[int, str, int, int, int, str | None]]) -> Self: + artist_ids: list[int] = [row[4] for row in rows] + first_row = rows[0] + + return cls( + first_row[0], + first_row[1], + first_row[2], + first_row[3], + artist_ids, + first_row[5] + ) + + def __str__(self) -> str: + return f'Title: {self.name}, Length: {self.length}, Album Id: {self.album_id}, Artist Ids: {self.artist_ids}, MBID: {self.mbid}' + + +class Connect: + def __init__(self, database: str): + self.con: sqlite3.Connection = sqlite3.connect(database) + + def __enter__(self) -> tuple[sqlite3.Connection, sqlite3.Cursor]: + return self.con, self.con.cursor() + + def __exit__(self, type, value, traceback): + self.con.commit() + self.con.close() + +# Takes a path to a .sql file and returns it's contents +def _load_query(path: str) -> str: + return open(path, 'r').read() + +def execute_query(query_path: str, qargs: list | tuple) -> list: + if not os.path.exists(query_path): + logging.error(f'Query Path "{query_path}" does not exist') + return [] + + query: str = _load_query(query_path) + + with Connect(DATABASE) as (conn, cur): + cur.execute(query, qargs) + + return cur.fetchall() + +def insert_listen_event(song_id: int, user_id: int, date: int | datetime.datetime, time: int): + pass + +def search_song_mbid(mbid: str) -> Song | None: + results: list = execute_query('sql/get_song_by_mbid.sql', (mbid,)) + + if results: + return Song.from_rows(results) + + return None + +def search_song_name(name: str) -> list[Song] | None: + results: list = execute_query('sql/get_song_by_name.sql', (name,)) + + if not results: + return None + + songs: list[Song] = [] + + current_song_id: int | None = None + rows = [] + for row in results: + if not current_song_id: + current_song_id = row[0] + rows.append(row) + + elif row[0] != current_song_id: + songs.append(Song.from_rows(rows)) + current_song_id = row[0] + rows = [row] + + else: + rows.append(row) + + return songs + +def search_song_by_id(id: int) -> Song | None: + results: list = execute_query('sql/get_song_by_id.sql', (id,)) + + if results: + return Song.from_rows(results) + + return None + +def insert_song(id: int, name: str, length: int, album_id: int, artist_id: int, mbid: str | None = None) -> None: + execute_query('sql/insert_song.sql', (id, name, length, album_id, artist_id, mbid)) + +def get_last_song_id() -> int: + return execute_query('sql/get_last_song_id.sql', [])[0][0] + +def search_album_by_name(name: str) -> list[Album] | None: + results: list = execute_query('sql/get_album_by_name.sql', (name,)) + + if not results: + return None + + albums: list[Album] = [] + for row in results: + albums.append(Album.from_row(row)) + +def search_album_by_mbid(mbid: str) -> Album | None: + pass + +def search_album_by_id(id: int) -> Album | None: + pass + +def insert_album(name: str, mbid: str | None, cover_art_url: str | None) -> None: + pass + +def search_artist_by_name(name: str) -> list[Artist] | None: + pass + +def search_artist_by_mbid(mbid: str) -> Artist | None: + pass + +def search_artist_by_id(id: int) -> Artist | None: + pass + +def insert_artist(name: str, mbid: str | None = None, icon_url: str | None = None) -> None: + pass + + + + diff --git a/SubSonic.py b/SubSonic.py index 71cb2a5..0c08675 100644 --- a/SubSonic.py +++ b/SubSonic.py @@ -477,3 +477,13 @@ class SubpyConn: response = response.json()['subsonic-response'] return response + + def getAlbum(self, album_id: str) -> dict: + url = self.base_url + '/rest/getAlbum?' + + payload = self._getBasePayload() + payload['id'] = album_id + + response = requests.get(url + self._encodePayload(payload)) + + return response.json()['subsonic-response'] diff --git a/main.py b/main.py index ef7211f..7674ebe 100644 --- a/main.py +++ b/main.py @@ -19,6 +19,12 @@ def main(): print(json.dumps(now_playing, indent=4)) + # album_id = now_playing['nowPlaying']['entry'][0]['albumId'] + + # album_info = conn.getAlbum(album_id) + + # print(json.dumps(album_info, indent=4)) + artist_info = conn.getArtist(now_playing['nowPlaying']['entry'][0]['artistId'], raw_json=True) print(json.dumps(artist_info, indent=4)) diff --git a/music-history.db b/music-history.db index 2eb4ae9..5e5d828 100644 Binary files a/music-history.db and b/music-history.db differ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..82c6d6f --- /dev/null +++ b/readme.md @@ -0,0 +1,10 @@ +# Config File Format + +.ini file +```ini +[Navidrome] +user = username +pass = password +url = https://navidrome-subdomain.my-domain.tld +``` + diff --git a/scrobble.py b/scrobble.py new file mode 100644 index 0000000..fc5ecb9 --- /dev/null +++ b/scrobble.py @@ -0,0 +1,71 @@ +import logging +import json +import re + +import musicbrainzngs +import flask + +import ListenBrainz + +logging.basicConfig( + format="%(levelname)s %(module)s::%(filename)s::%(funcName)s %(asctime)s - %(message)s", + level=logging.DEBUG +) + +app = flask.Flask(__name__) + +musicbrainzngs.set_useragent("Navidrome Scrobbler", "beta 0.0") + +# Returns a tuple of (track_mbid, album_mbid, artist_mbids: list) +# TODO improve matching instead of assuming the top result is correct +def get_mbid(scrobble: ListenBrainz.ListenBrainzScrobble) -> tuple[str, str, list[str]]: + optional_args = {} + if scrobble.duration: + optional_args['dur'] = scrobble.duration * 1000 + elif scrobble.duration_ms: + optional_args['dur'] = scrobble.duration_ms + + result = musicbrainzngs.search_recordings(f"{scrobble.track_name} {scrobble.release_name}", artist=scrobble.artist_name, **optional_args) + + top_result: dict = result['recording-list'][0] + + track_mbid: str = top_result['id'] + album_mbid: str = top_result['release-list'][0]['id'] + artist_mbids: list[str] = [artist['artist']['id'] for artist in top_result['artist-credit']] + + return (track_mbid, album_mbid, artist_mbids) + +# See https://listenbrainz.readthedocs.io/en/latest/users/json.html#json-doc +@app.route('/1/submit-listens', methods=["POST"]) +def submit_listens(): + + if 'Authorization' not in flask.request.headers: + return '', 401 + + user_token_match: re.Match[str] | None = re.match(r'Token (.*)', flask.request.headers['Authorization']) + + if not user_token_match: + return '', 401 + + user_token: str = user_token_match.group(1) + + request_json: dict | None = flask.request.json + if not request_json: + return '', 401 + + listen_type: str = request_json['listen_type'] + payload: list[dict] = request_json['payload'] + + logging.debug("User Token", user_token) + for scrobble_data in payload: + logging.debug("Recievied scrobble", json.dumps(scrobble_data, indent=4)) + + scrobble = ListenBrainz.ListenBrainzScrobble.from_json(scrobble_data) + + + + + return '', 200 + +if __name__ == '__main__': + app.run('127.0.0.1', port=8545) diff --git a/sql/get_album.sql b/sql/get_album.sql new file mode 100644 index 0000000..125e795 --- /dev/null +++ b/sql/get_album.sql @@ -0,0 +1 @@ +SELECT * FROM albums WHERE id = ?; \ No newline at end of file diff --git a/sql/get_album_by_name.sql b/sql/get_album_by_name.sql new file mode 100644 index 0000000..ce27341 --- /dev/null +++ b/sql/get_album_by_name.sql @@ -0,0 +1 @@ +SELECT * FROM albums WHERE name = ?; \ No newline at end of file diff --git a/sql/get_album_id.sql b/sql/get_album_id.sql deleted file mode 100644 index edf1024..0000000 --- a/sql/get_album_id.sql +++ /dev/null @@ -1,3 +0,0 @@ -SELECT * -FROM albums -WHERE name = ?; diff --git a/sql/get_artist.sql b/sql/get_artist.sql new file mode 100644 index 0000000..15edb56 --- /dev/null +++ b/sql/get_artist.sql @@ -0,0 +1 @@ +SELECT * FROM artists WHERE id = ?; \ No newline at end of file diff --git a/sql/get_artist_id.sql b/sql/get_artist_id.sql deleted file mode 100644 index bfebba4..0000000 --- a/sql/get_artist_id.sql +++ /dev/null @@ -1,3 +0,0 @@ -SELECT * -FROM artists -WHERE name = ?; diff --git a/sql/get_last_song_id.sql b/sql/get_last_song_id.sql new file mode 100644 index 0000000..87957c7 --- /dev/null +++ b/sql/get_last_song_id.sql @@ -0,0 +1 @@ +SELECT id FROM songs ORDER BY id DESC LIMIT 1; \ No newline at end of file diff --git a/sql/get_latest_song_id.sql b/sql/get_latest_song_id.sql deleted file mode 100644 index 07b195c..0000000 --- a/sql/get_latest_song_id.sql +++ /dev/null @@ -1,4 +0,0 @@ -SELECT * -FROM songs -ORDER BY id -DESC LIMIT 1; diff --git a/sql/get_song_by_id.sql b/sql/get_song_by_id.sql new file mode 100644 index 0000000..400a1df --- /dev/null +++ b/sql/get_song_by_id.sql @@ -0,0 +1 @@ +SELECT * FROM songs WHERE id = ?; \ No newline at end of file diff --git a/sql/get_song_by_mbid.sql b/sql/get_song_by_mbid.sql new file mode 100644 index 0000000..23ee98c --- /dev/null +++ b/sql/get_song_by_mbid.sql @@ -0,0 +1 @@ +SELECT * FROM songs WHERE musicBrainzId = ?; \ No newline at end of file diff --git a/sql/get_song_by_name.sql b/sql/get_song_by_name.sql new file mode 100644 index 0000000..dcadc0c --- /dev/null +++ b/sql/get_song_by_name.sql @@ -0,0 +1 @@ +SELECT * FROM songs WHERE name = ? ORDER BY id ASC; \ No newline at end of file diff --git a/sql/get_song_id.sql b/sql/get_song_id.sql deleted file mode 100644 index 34909df..0000000 --- a/sql/get_song_id.sql +++ /dev/null @@ -1,4 +0,0 @@ -SELECT * -FROM songs -WHERE name = ? -AND artist = ?; diff --git a/sql/get_user_id.sql b/sql/get_user_id.sql deleted file mode 100644 index 5c20eab..0000000 --- a/sql/get_user_id.sql +++ /dev/null @@ -1,3 +0,0 @@ -SELECT * -FROM users -WHERE name = ?; diff --git a/sql/insert_album.sql b/sql/insert_album.sql index fcdf592..4f45dcc 100644 --- a/sql/insert_album.sql +++ b/sql/insert_album.sql @@ -1,3 +1 @@ -INSERT INTO - albums (name, spotify_id, cover_art_url) -VALUES (?, ?, ?); +INSERT INTO albums (id, name, musicBrainzId, cover_art_url) VALUES (?, ?, ?, ?); \ No newline at end of file diff --git a/sql/insert_artist.sql b/sql/insert_artist.sql index cadc0bb..24da520 100644 --- a/sql/insert_artist.sql +++ b/sql/insert_artist.sql @@ -1,3 +1 @@ -INSERT INTO - artists (name, spotify_id, icon_url) -VALUES (?, ?, ?); +INSERT INTO artists (id, name, musicBrainzId, icon_url) VALUES (?, ?, ?, ?); \ No newline at end of file diff --git a/sql/insert_listen_event.sql b/sql/insert_listen_event.sql new file mode 100644 index 0000000..3358d43 --- /dev/null +++ b/sql/insert_listen_event.sql @@ -0,0 +1 @@ +INSERT INTO "listen-events" (sond_id, user_id, date, time) VALUES (?, ?, ?, ?); \ No newline at end of file diff --git a/sql/insert_song.sql b/sql/insert_song.sql index ba74b53..4780c3f 100644 --- a/sql/insert_song.sql +++ b/sql/insert_song.sql @@ -1,4 +1 @@ --- With Spotify Id -INSERT INTO - songs (id, name, length, album, artist, spotify_id) -VALUES (?, ?, ?, ?, ?, ?); +INSERT INTO songs (id, name, length, album_id, artist_id, musicBrainzId) VALUES (?, ?, ?, ?, ?, ?); \ No newline at end of file diff --git a/test-database.py b/test-database.py new file mode 100644 index 0000000..67ee423 --- /dev/null +++ b/test-database.py @@ -0,0 +1,11 @@ +import MusicDatabase + + + +print(MusicDatabase.search_song_by_id(1)) +print(MusicDatabase.get_last_song_id()) + + + + + diff --git a/test.py b/test.py new file mode 100644 index 0000000..be4faff --- /dev/null +++ b/test.py @@ -0,0 +1,10 @@ +import sqlite3 +import json + +import musicbrainzngs + +musicbrainzngs.set_useragent("spotify matcher", "beta 0.0") +result = musicbrainzngs.search_recordings("Misao pandemic something worth doing!", artist="psiangel", dur="179367") + +print(json.dumps(result['recording-list'][0], indent=4)) + diff --git a/test2.py b/test2.py new file mode 100644 index 0000000..3ee8127 --- /dev/null +++ b/test2.py @@ -0,0 +1,11 @@ +args = { + "foo" : 1, + "bar" : 2 +} + +def baz(a, b, buz = None, foo = None, bar = None): + print(foo, bar) + +baz(0,0, **args) + +