added a bunch of stuff
This commit is contained in:
parent
5aaf6ede5d
commit
95c4dc96f6
60
ListenBrainz.py
Normal file
60
ListenBrainz.py
Normal file
@ -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}
|
||||||
|
)
|
||||||
162
MusicDatabase.py
Normal file
162
MusicDatabase.py
Normal file
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
10
SubSonic.py
10
SubSonic.py
@ -477,3 +477,13 @@ class SubpyConn:
|
|||||||
response = response.json()['subsonic-response']
|
response = response.json()['subsonic-response']
|
||||||
|
|
||||||
return 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']
|
||||||
|
|||||||
6
main.py
6
main.py
@ -19,6 +19,12 @@ def main():
|
|||||||
|
|
||||||
print(json.dumps(now_playing, indent=4))
|
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)
|
artist_info = conn.getArtist(now_playing['nowPlaying']['entry'][0]['artistId'], raw_json=True)
|
||||||
|
|
||||||
print(json.dumps(artist_info, indent=4))
|
print(json.dumps(artist_info, indent=4))
|
||||||
|
|||||||
BIN
music-history.db
BIN
music-history.db
Binary file not shown.
10
readme.md
Normal file
10
readme.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Config File Format
|
||||||
|
|
||||||
|
.ini file
|
||||||
|
```ini
|
||||||
|
[Navidrome]
|
||||||
|
user = username
|
||||||
|
pass = password
|
||||||
|
url = https://navidrome-subdomain.my-domain.tld
|
||||||
|
```
|
||||||
|
|
||||||
71
scrobble.py
Normal file
71
scrobble.py
Normal file
@ -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)
|
||||||
1
sql/get_album.sql
Normal file
1
sql/get_album.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
SELECT * FROM albums WHERE id = ?;
|
||||||
1
sql/get_album_by_name.sql
Normal file
1
sql/get_album_by_name.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
SELECT * FROM albums WHERE name = ?;
|
||||||
@ -1,3 +0,0 @@
|
|||||||
SELECT *
|
|
||||||
FROM albums
|
|
||||||
WHERE name = ?;
|
|
||||||
1
sql/get_artist.sql
Normal file
1
sql/get_artist.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
SELECT * FROM artists WHERE id = ?;
|
||||||
@ -1,3 +0,0 @@
|
|||||||
SELECT *
|
|
||||||
FROM artists
|
|
||||||
WHERE name = ?;
|
|
||||||
1
sql/get_last_song_id.sql
Normal file
1
sql/get_last_song_id.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
SELECT id FROM songs ORDER BY id DESC LIMIT 1;
|
||||||
@ -1,4 +0,0 @@
|
|||||||
SELECT *
|
|
||||||
FROM songs
|
|
||||||
ORDER BY id
|
|
||||||
DESC LIMIT 1;
|
|
||||||
1
sql/get_song_by_id.sql
Normal file
1
sql/get_song_by_id.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
SELECT * FROM songs WHERE id = ?;
|
||||||
1
sql/get_song_by_mbid.sql
Normal file
1
sql/get_song_by_mbid.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
SELECT * FROM songs WHERE musicBrainzId = ?;
|
||||||
1
sql/get_song_by_name.sql
Normal file
1
sql/get_song_by_name.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
SELECT * FROM songs WHERE name = ? ORDER BY id ASC;
|
||||||
@ -1,4 +0,0 @@
|
|||||||
SELECT *
|
|
||||||
FROM songs
|
|
||||||
WHERE name = ?
|
|
||||||
AND artist = ?;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
SELECT *
|
|
||||||
FROM users
|
|
||||||
WHERE name = ?;
|
|
||||||
@ -1,3 +1 @@
|
|||||||
INSERT INTO
|
INSERT INTO albums (id, name, musicBrainzId, cover_art_url) VALUES (?, ?, ?, ?);
|
||||||
albums (name, spotify_id, cover_art_url)
|
|
||||||
VALUES (?, ?, ?);
|
|
||||||
@ -1,3 +1 @@
|
|||||||
INSERT INTO
|
INSERT INTO artists (id, name, musicBrainzId, icon_url) VALUES (?, ?, ?, ?);
|
||||||
artists (name, spotify_id, icon_url)
|
|
||||||
VALUES (?, ?, ?);
|
|
||||||
1
sql/insert_listen_event.sql
Normal file
1
sql/insert_listen_event.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
INSERT INTO "listen-events" (sond_id, user_id, date, time) VALUES (?, ?, ?, ?);
|
||||||
@ -1,4 +1 @@
|
|||||||
-- With Spotify Id
|
INSERT INTO songs (id, name, length, album_id, artist_id, musicBrainzId) VALUES (?, ?, ?, ?, ?, ?);
|
||||||
INSERT INTO
|
|
||||||
songs (id, name, length, album, artist, spotify_id)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?);
|
|
||||||
11
test-database.py
Normal file
11
test-database.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import MusicDatabase
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print(MusicDatabase.search_song_by_id(1))
|
||||||
|
print(MusicDatabase.get_last_song_id())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
10
test.py
Normal file
10
test.py
Normal file
@ -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))
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user