diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 879cd05..ac89dbc 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -8,5 +8,17 @@ jdbc:sqlite:/tmp/music-downloader/metadata.db $ProjectFileDir$ + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/src/test.db + $ProjectFileDir$ + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.39.2/sqlite-jdbc-3.39.2.jar + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..af742c7 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/setup.py b/setup.py index 57c24b0..9300be3 100644 --- a/setup.py +++ b/setup.py @@ -43,8 +43,8 @@ with open('version', 'r') as version_file: setup( name='music-kraken', version=version, - description='An extensive music downloader crawling the internet. It gets its metadata from a couple metadata ' - 'provider, and it scrapes the audiofiles.', + description='An extensive music downloader crawling the internet. It gets its metadata from a couple of metadata ' + 'providers, and it scrapes the audiofiles.', long_description=long_description, long_description_content_type='text/markdown', author='Hellow2', diff --git a/src/.fuse_hidden0000823600000006 b/src/.fuse_hidden0000823600000006 new file mode 100644 index 0000000..a4a79ff Binary files /dev/null and b/src/.fuse_hidden0000823600000006 differ diff --git a/src/goof.py b/src/goof.py index e0808b8..a024391 100644 --- a/src/goof.py +++ b/src/goof.py @@ -8,51 +8,33 @@ from music_kraken import ( import music_kraken.database.new_database as db + cache = music_kraken.database.new_database.Database("test.db") cache.reset() song = Song( title="Vein Deep in the Solution", release_name="One Final Action", + length=666, target=Target(file="~/Music/genre/artist/album/song.mp3", path="~/Music/genre/artist/album"), metadata={ "album": "One Final Action" }, lyrics=[ - Lyrics(text="these are some depressive lyrics", language="en") + Lyrics(text="these are some depressive lyrics", language="en"), + Lyrics(text="test", language="en") ], sources=[ - Source(src="youtube", url="https://youtu.be/dfnsdajlhkjhsd") + Source(src="youtube", url="https://youtu.be/dfnsdajlhkjhsd"), + Source(src="musify", url="https://ln.topdf.de/Music-Kraken/") ] ) -cache.push([song]) +song_ref = song.reference +print(song_ref) -""" -music_kraken.clear_cache() +lyrics = Lyrics(text="these are some Lyrics that don't belong to any Song", language="en") -artist = music_kraken.Artist( - name="I'm in a Coffin" -) +cache.push([song, lyrics]) -song = Song( - title="Vein Deep in the Solution", - release_name="One Final Action", - target=Target(file="~/Music/genre/artist/album/song.mp3", path="~/Music/genre/artist/album"), - metadata={ - "album": "One Final Action" - }, - lyrics=[ - Lyrics(text="these are some depressive lyrics", language="en") - ], - sources=[ - Source(src="youtube", url="https://youtu.be/dfnsdajlhkjhsd") - ] -) - - -print(song) -print(song.id) - -# music_kraken.fetch_sources([song]) -""" +cache.pull_single_song(song_ref=song_ref) diff --git a/src/music_kraken/database/new_database.py b/src/music_kraken/database/new_database.py index 077ebda..c875e39 100644 --- a/src/music_kraken/database/new_database.py +++ b/src/music_kraken/database/new_database.py @@ -1,9 +1,10 @@ import sqlite3 import os import logging -from typing import List +from typing import List, Tuple from pkg_resources import resource_string +from .objects.database_object import Reference from .objects import ( Song, Lyrics, @@ -15,12 +16,34 @@ from .objects import ( logger = logging.getLogger("database") +# Due to this not being deployed on a Server **HOPEFULLY** +# I don't need to parameterize stuff like the where and +# use complicated query builder +SONG_QUERY = """ +SELECT +Song.id AS song_id, Song.name AS title, Song.isrc AS isrc, Song.length AS length, +Target.id AS target_id, Target.file AS file, Target.path AS path +FROM Song +LEFT JOIN Target ON Song.id=Target.song_id +WHERE Song.id="{song_id}"; +""" +SOURCE_QUERY = """ +SELECT id, src, url, song_id +FROM Source +WHERE {where}; +""" +LYRICS_QUERY = """ +SELECT id, text, language, song_id +FROM Lyrics +WHERE {where}; +""" + class Database: def __init__(self, database_file: str): self.database_file: str = database_file + self.connection, self.cursor = self.reset_cursor() - self.connection = sqlite3.connect(self.database_file) self.cursor = self.connection.cursor() def reset(self): @@ -36,14 +59,21 @@ class Database: os.remove(self.database_file) # newly creating the database - self.connection = sqlite3.connect(self.database_file) - self.cursor = self.connection.cursor() + self.reset_cursor() query = resource_string("music_kraken", "static_files/new_db.sql").decode('utf-8') # fill the database with the schematic self.cursor.executescript(query) self.connection.commit() + def reset_cursor(self) -> Tuple[sqlite3.Connection, sqlite3.Cursor]: + self.connection = sqlite3.connect(self.database_file) + # This is necessary that fetching rows returns dicts instead of tuple + self.connection.row_factory = sqlite3.Row + + self.cursor = self.connection.cursor() + return self.connection, self.cursor + def push_one(self, db_object: Song | Lyrics | Target | Artist | Source): if type(db_object) == Song: return self.push_song(song=db_object) @@ -82,27 +112,143 @@ class Database: name - title """ table = "Song" - query = f"INSERT OR REPLACE INTO {table} (id, name) VALUES (?, ?);" + query = f"INSERT OR REPLACE INTO {table} (id, name, isrc, length) VALUES (?, ?, ?, ?);" values = ( song.id, - song.title + song.title, + song.isrc, + song.length ) self.cursor.execute(query, values) self.connection.commit() - def push_lyrics(self, lyrics: Lyrics): - pass + # add sources + for source in song.sources: + self.push_source(source=source) + + # add lyrics + for single_lyrics in song.lyrics: + self.push_lyrics(lyrics=single_lyrics) + + # add target + self.push_target(target=song.target) + + def push_lyrics(self, lyrics: Lyrics, ): + if lyrics.song_ref_id is None: + logger.warning("the Lyrics don't refer to a song") + + table = "Lyrics" + query = f"INSERT OR REPLACE INTO {table} (id, song_id, text, language) VALUES (?, ?, ?, ?);" + values = ( + lyrics.id, + lyrics.song_ref_id, + lyrics.text, + lyrics.language + ) + + self.cursor.execute(query, values) + self.connection.commit() + + def push_source(self, source: Source): + if source.song_ref_id is None: + logger.warning("the Source don't refer to a song") + + table = "Source" + query = f"INSERT OR REPLACE INTO {table} (id, song_id, src, url) VALUES (?, ?, ?, ?);" + values = ( + source.id, + source.song_ref_id, + source.src, + source.url + ) + + self.cursor.execute(query, values) + self.connection.commit() def push_target(self, target: Target): - pass + if target.song_ref_id is None: + logger.warning("the Target doesn't refer to a song") + + table = "Target" + query = f"INSERT OR REPLACE INTO {table} (id, song_id, file, path) VALUES (?, ?, ?, ?);" + values = ( + target.id, + target.song_ref_id, + target.file, + target.path + ) + + self.cursor.execute(query, values) + self.connection.commit() def push_artist(self, artist: Artist): pass - def push_source(self, source: Source): + def pull_lyrics(self, song_ref: Reference = None, lyrics_ref: Reference = None) -> List[Lyrics]: pass + def pull_sources(self, song_ref: Reference = None, source_ref: Reference = None) -> List[Source]: + """ + Gets a list of sources. if source_ref is passed in the List will most likely only + contain one Element if everything goes accordingly. + **If neither song_ref nor source_ref are passed in it will return ALL sources** + :param song_ref: + :param source_ref: + :return: + """ + + where = "1=1" + if song_ref is not None: + where = f"song_id=\"{song_ref.id}\"" + elif source_ref is not None: + where = f"id=\"{source_ref.id}\"" + + query = SOURCE_QUERY.format(where=where) + self.cursor.execute(query) + + source_rows = self.cursor.fetchall() + return [Source( + id_=source_row['id'], + src=source_row['src'], + url=source_row['url'] + ) for source_row in source_rows] + + def pull_single_song(self, song_ref: Reference = None) -> Song: + """ + This function is used to get one song (including its children like Sources etc) + from one song id (a reference object) + :param song_ref: + :return requested_song: + """ + if song_ref.id is None: + raise ValueError("The Song ref doesn't point anywhere. Remember to use the debugger.") + query = SONG_QUERY.format(song_id=song_ref.id) + self.cursor.execute(query) + + song_rows = self.cursor.fetchall() + if len(song_rows) == 0: + logger.warning(f"No song found for the id {song_ref.id}") + return Song() + if len(song_rows) > 1: + logger.warning(f"Multiple Songs found for the id {song_ref.id}. Defaulting to the first one.") + song_result = song_rows[0] + + song = Song( + id_=song_result['song_id'], + title=song_result['title'], + isrc=song_result['isrc'], + length=song_result['length'], + target=Target( + id_=song_result['target_id'], + file=song_result['file'], + path=song_result['path'] + ), + sources=self.pull_sources(song_ref=song_ref) + ) + + return song + if __name__ == "__main__": cache = Database("") diff --git a/src/music_kraken/database/objects/database_object.py b/src/music_kraken/database/objects/database_object.py index 5d5dc31..af46975 100644 --- a/src/music_kraken/database/objects/database_object.py +++ b/src/music_kraken/database/objects/database_object.py @@ -4,15 +4,19 @@ from ...utils.shared import ( SONG_LOGGER as logger ) + class Reference: def __init__(self, id_: str) -> None: self.id = id_ + def __str__(self): + return f"references to an object with the id: {self.id}" + class DatabaseObject: def __init__(self, id_: str = None) -> None: self.id_: str | None = id_ - + def get_id(self) -> str: """ returns the id if it is set, else diff --git a/src/music_kraken/database/objects/song.py b/src/music_kraken/database/objects/song.py index c9be1dc..d873d09 100644 --- a/src/music_kraken/database/objects/song.py +++ b/src/music_kraken/database/objects/song.py @@ -12,13 +12,33 @@ from .database_object import ( ) +class SongAttribute: + def __init__(self, song_ref: Reference = None): + # the reference to the song the lyrics belong to + self.song_ref = song_ref + + def add_song(self, song_ref: Reference): + self.song_ref = song_ref + + def get_ref_song_id(self): + if self.song_ref is None: + return None + return self.song_ref.id + + def set_ref_song_id(self, song_id): + self.song_ref = Reference(song_id) + + song_ref_id = property(fget=get_ref_song_id, fset=set_ref_song_id) + + class Metadata: """ Shall only be read or edited via the Song object. For this reason there is no reference to the song needed. """ + def __init__(self, data: dict = {}) -> None: - self.data = {} + self.data = data def get_all_metadata(self): return list(self.data.items()) @@ -33,7 +53,7 @@ class Metadata: return self.data[item] -class Source(DatabaseObject): +class Source(DatabaseObject, SongAttribute): """ create somehow like that ```python @@ -41,23 +61,30 @@ class Source(DatabaseObject): Source(src="youtube", url="https://youtu.be/dfnsdajlhkjhsd") ``` """ + def __init__(self, id_: str = None, src: str = None, url: str = None) -> None: - super().__init__(id_=id_) + DatabaseObject.__init__(self, id_=id_) + SongAttribute.__init__(self) self.src = src self.url = url + def __str__(self): + return f"{self.src}: {self.url}" -class Target(DatabaseObject): + +class Target(DatabaseObject, SongAttribute): """ create somehow like that ```python - # I know path is pointles, and I will change that (don't worry about backwards compatibility there) + # I know path is pointless, and I will change that (don't worry about backwards compatibility there) Target(file="~/Music/genre/artist/album/song.mp3", path="~/Music/genre/artist/album") ``` """ - def __init__(self, id_:str = None, file: str = None, path: str = None) -> None: - super().__init__(id_=id_) + + def __init__(self, id_: str = None, file: str = None, path: str = None) -> None: + DatabaseObject.__init__(self, id_=id_) + SongAttribute.__init__(self) self._file = file self._path = path @@ -86,7 +113,7 @@ class Target(DatabaseObject): return False return os.path.exists(self.file) - + def is_set(self) -> bool: return not (self._file is None or self._path is None) @@ -96,30 +123,31 @@ class Target(DatabaseObject): exists_on_disc = property(fget=get_exists_on_disc) -class Lyrics(DatabaseObject): +class Lyrics(DatabaseObject, SongAttribute): def __init__(self, text: str, language: str, id_: str = None) -> None: - super().__init__(id_=id_) + DatabaseObject.__init__(self, id_=id_) + SongAttribute.__init__(self) self.text = text self.language = language class Song(DatabaseObject): def __init__( - self, - id_: str = None, - mb_id: str = None, - title: str = None, - release_name: str = None, - artist_names: List[str] = [], - isrc: str = None, - length: int = None, - sources: List[Source] = None, - target: Target = None, - lyrics: List[Lyrics] = None, - metadata: dict = {}, - release_ref: str = None, - artist_refs: List[Reference] = None - ) -> None: + self, + id_: str = None, + mb_id: str = None, + title: str = None, + release_name: str = None, + artist_names: List[str] = [], + isrc: str = None, + length: int = None, + sources: List[Source] = None, + target: Target = None, + lyrics: List[Lyrics] = None, + metadata: dict = {}, + release_ref: str = None, + artist_refs: List[Reference] = None + ) -> None: """ id: is not NECESARRILY the musicbrainz id, but is DISTINCT for every song mb_id: is the musicbrainz_id @@ -133,29 +161,33 @@ class Song(DatabaseObject): self.title: str | None = title self.release_name: str | None = release_name self.isrc: str | None = isrc - self.length: int | None = length + self.length_: int | None = length self.artist_names = artist_names self.metadata = Metadata(data=metadata) - if sources is None: sources = [] self.sources: List[Source] = sources - + for source in self.sources: + source.add_song(self.reference) + if target is None: target = Target() self.target: Target = target + self.target.add_song(self.reference) if lyrics is None: lyrics = [] self.lyrics: List[Lyrics] = lyrics + for lyrics_ in self.lyrics: + lyrics_.add_song(self.reference) self.release_ref = release_ref self.artist_refs = artist_refs def __str__(self) -> str: - return f"\"{self.title}\" by {', '.join([str(a) for a in self.artists])}" + return f"\"{self.title}\" by {', '.join(self.artist_names)}" def __repr__(self) -> str: return self.__str__() @@ -167,7 +199,20 @@ class Song(DatabaseObject): return self.isrc is not None def get_artist_names(self) -> List[str]: - return [a.name for a in self.artists] + return self.artist_names + + def get_length(self): + if self.length_ is None: + return None + return int(self.length_) + + def set_length(self, length: int): + if type(length) != int: + raise TypeError(f"length of a song must be of the type int not {type(length)}") + self.length_ = length + + length = property(fget=get_length, fset=set_length) + if __name__ == "__main__": """ diff --git a/src/music_kraken/static_files/new_db.sql b/src/music_kraken/static_files/new_db.sql index 7cc54c6..9db6f26 100644 --- a/src/music_kraken/static_files/new_db.sql +++ b/src/music_kraken/static_files/new_db.sql @@ -1,14 +1,20 @@ CREATE TABLE Song ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name TEXT + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name TEXT, + isrc TEXT, + length INT -- length is in milliseconds (could be wrong) ); CREATE TABLE Source ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - song_id BIGINT, + id BIGINT AUTO_INCREMENT PRIMARY KEY, + src TEXT NOT NULL, + url TEXT NOT NULL, + certainty INT NOT NULL DEFAULT 0, -- certainty=0 -> it is definitely a valid source + valid BOOLEAN NOT NULL DEFAULT 1, + song_id BIGINT, FOREIGN KEY(song_id) REFERENCES Song(id) ); @@ -29,14 +35,18 @@ CREATE TABLE Album CREATE TABLE Target ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - song_id BIGINT, + file TEXT NOT NULL, + path TEXT, + song_id BIGINT UNIQUE, FOREIGN KEY(song_id) REFERENCES Song(id) ); CREATE TABLE Lyrics ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - song_id BIGINT, + id BIGINT AUTO_INCREMENT PRIMARY KEY, + text TEXT, + language TEXT, + song_id BIGINT, FOREIGN KEY(song_id) REFERENCES Song(id) ); @@ -55,9 +65,3 @@ CREATE TABLE AlbumArtist FOREIGN KEY(album_id) REFERENCES Album(id), FOREIGN KEY(artist_id) REFERENCES Artist(id) ); - - -SELECT - Song.id, - Song.name -FROM Song diff --git a/src/test.db b/src/test.db new file mode 100644 index 0000000..8043063 Binary files /dev/null and b/src/test.db differ