From 4b60ed75552d1e18a947fcb8c809ff7f22d145a6 Mon Sep 17 00:00:00 2001 From: Hellow Date: Tue, 6 Dec 2022 23:44:42 +0100 Subject: [PATCH] much progress on new db integration --- .idea/dataSources.xml | 12 ++ .idea/sqldialects.xml | 6 + setup.py | 4 +- src/.fuse_hidden0000823600000006 | Bin 0 -> 65536 bytes src/goof.py | 40 ++--- src/music_kraken/database/new_database.py | 166 ++++++++++++++++-- .../database/objects/database_object.py | 6 +- src/music_kraken/database/objects/song.py | 105 +++++++---- src/music_kraken/static_files/new_db.sql | 30 ++-- src/test.db | Bin 0 -> 65536 bytes 10 files changed, 284 insertions(+), 85 deletions(-) create mode 100644 .idea/sqldialects.xml create mode 100644 src/.fuse_hidden0000823600000006 create mode 100644 src/test.db 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 0000000000000000000000000000000000000000..a4a79ffa1b59218bfd37c7e2c3c551bb3b01664a GIT binary patch literal 65536 zcmeI*&2Q6Y90zbarA?c@1P&;wrb!RtKr3)#=cSG}F$iE#M@XTK*|;q7%ag?5IGyb< zdYRNtJM6Ukj?)giao_)drX42jwtr*VX^-usZ2}ax6H+R^j}qJQz^E@`SACqi9 zs@VZ0t)AD_15)J9b3D)8B!uI*CH6kg-orG@Ce95%urYsR`fZa-+}anPFJ}MYva^@C z>_YbUMR)1%g;$H!h19~&nLpKOfQ)eCGtI5K(u_Lf?0sH9Q7Ra424jLb@gL`e_JHEoN1xV3R_qu#hS%!t+% z=CGOSJ#}lCad&e|-MCjL_thsW!_uo{OTDXZsrB3HcD$Jt+gg2NiQ#HTi$rt7GMUxA zhfC?)rAz$&vna6bp4*P&I-JUmgmpOoAS#~~I(1Nk!XrBvZsUK1)L2O8mY4bOrlOE) zyPj?O@o;e@lz84CjHpuLS{l?1scMbQ=0=^Bd7#!C8Gf1E)aOS2G<7p`U!EG)$f&+mPlPUmttet-GM0UjQ$vm;#5K|R)D5^4=w zz|jLgdE~3JGr8-zx7B{MTTLk%&3V&N)$iSiBZ<#a(QsxomT2BsFoV<2adl6kbC#=j z4=SHE&Y9z^<1-4|zi5l;eRnF_!|&z6Ij8TLbh{T0&yDP1Ja23d!)BF~+%vIsTb(B8|<=$7|TH}xuf2#YPALtudy6KAO{Ox7m4s0u>)fJ#G)CFxYPJ6Q`la~r7&e@6>C-iytLKiZomjak#pCI{ zn|wNV@gl!(4r<43Z?p4pYOH45_E^2VyG~wjIB|fsAJ^02+0vnIJ3N7e>|tWagIP@0 zc1hQE9NVX6&$WEAV)puuMGQ*5@z{>Cf7Gs|W^$iieA~eiMiWNswx`&i7vc3hx5(an z_BW2bkRSj72tWV=5P$##AOHafKmY;|m^gtu{Iqt1OUX(}5sOkm&=pz{N-`CNil`KX zvRF_ovusqva`7=`cMk4Qx)a_(2s)H(_ndxU_uSM^85S3x{flESBnUtN0uX=z1Rwwb z2tWV=5P$##&X~ZJDQzyvt%0!rKb5_}Wq)7;BnUtN0uX=z1Rwwb2tWV=5P-mkBJezw z)|NMC=QPPM%4JKJ1j(u>LP=3%p(0zBAQoxMTsL*8QWCp;-)`-8f?&u0B%gQOwV=0S zwbm?}f537~;l8Ioqi#OQg?OJ&YaiWb+g0R}R8$Iv&{9k)l*)yYV5}=eK{jPJuB?|N zD)09CLEoUm4M!i>44SuEu5am2ozAnT9p6f_*|7gVm3_%&U*ZXX52cACP6$8%0uX=z z1Rwwb2tWV=5SVy@l_`x+a$_PD_WvijlMwd*k9^DD#1mi#ApijgKmY;|fB*y_009U< z00Iy=sX*BOkL&-Fs$qEuKmY;|fB*y_009U<00Izzz}XT&|Nq&lFYGM@AOHafKmY;| zfB*y_009U<;4}j0|38fy%0K`D5P$##AOHafKmY;|fB*!}mO$A5&u1TT?1cmY2tWV= z5P$##AOHafKmY;|fIw1#y=(lm);BdtFBUALUno_yz{}fB*y_009U<00Izz00bZq3VfBC*8B@x zrC1V+)Q|*PvJ9bA)GI<|-KYq~a;YU!Dwe4v2Oa8DqI;D1>?waz+x2YICqYLKh}Cni z1jL{Yd#0ZRJ)*n2WSdP>x5xhTz=I1~(PV3~3bJ6V7c8M9nk7ML(Uu^K>t#!lO~n#h zW9zr*jz@jpeol!KZHl@{uK&aS|5WxRd;XsdkRSj72tWV=5P$##AOHafKmY;|m;`~9 zDUDBZ^C1=X|0lVNfZzX{glfe;KmY;|fB*y_009U<00Izz00iEz0IvVvuP@dP0SG_< V0uX=z1Rwwb2tWV=5SRpk{{SF>XB7Yd literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8043063737df059bf3955a6ef5bc0bdeefb599a9 GIT binary patch literal 65536 zcmeI*%}?8A90zbaDFnic%K=4bn)Gx%6x5o;d3UFdL7P^rG_(+R>t)C`~HYd+F_b@+Mh9LmpzZ20AVdN>_XJ9mDr9SKk@VXJdaK6#|fK{ z)|`+MyW{WZAt?!$1yK|}CPWYfi9Ii~XMY%DGne`=*i<|=|F&64Sp4*pDd{gk8v8(y zrlen{I@5nnzMfi{Oi%uh{dMBEiJ!(_XMY^~nWv6le=<7Ja`rbw%PI8aa>1p(m+^8%MYUpSf+Ca)yq6kK)#Lt5mIY1lbf3pzfMOX zt?l`a8N}nMfl%UQy)dFmiEF7>H)L6Bt#7S1S)K=tW{W)BSbea(@q|}JY#q`UVJ`=1 zH9qTArmK7HuHL3R1}z26!?ZqU;GHVxx(cEF&wEN-BEgSc(}XZ*I~9!5!;>n+mM*4AR0T|L}>GeGaX zwdU$0tr4$$27`&y4B}X}it%*vdN#qzO1$#K{`;9sE|(Jz=8o;)d~Y2a;EHzY6D=m8 zR=))t-|>@2zA!eLyOn!e?MKDhQliPYKN3~_{!$!Ce3XjDql2+T%T5H-JN%qh_ar)I zdHT*_<+H{)dYW~7L}BHNwwO8a(oqS&sr8OIUEide9X`G^P{MfGi4yW=m6Y5!`P$LI zcf}KpwClV5)M!sVk!(^w)EzI}V=FP1!&Gvoad%l;YmveYmLkjKaK&2%B7a1jkT?Oc zsO@-cA9eSLPM+&7+qZ%!#7Hk*5N~~DeSNL5-2C_ZEAj5pZ#aDI^EIC9mrkplSb0l{ zrz87IVkS2`D<0^*+VR?(?0B3yQ8QlqM7<=eljH3sj?W3=YC1Yty3}j);|Evg12gU| zVz7=!b{yAr0%~?VDKy>-Bm~RqcxUn6fJecPQQER}jK&N;W%gH*`8)>iaBW*aHIuAOHafKmY;| zfB*y_009U<00KiMa4oHkC%H7h`~OqYj39l>Mi?Lf0SG_<0uX=z1Rwwb2tWV=?~1_l zR7RUy9~;+-x@A~)r6{WvHY!(aMb_1NL9Q0+RnxMob;~e!x`AWwZHM7*a5JBGy~VJz zYuSqy%|Bo%rhMPmpHVNLH$CKs%d!M0ne%6i$>Wm;1eQ&kF8 zv#9KKx?$I#{S8Mi7Y&-XY%j3%r|$N%r`v&*WV5{gKP9~qq*u5D;9Y6rh!X-3fB*y_ z009U<00Izz00b_)zF@uG?5F?n>;FkB z80$d*0uX=z1Rwwb2tWV=5P$##E{*{I{r@DFAo%_NIRC#m^@fr_00Izz00bZa0SG_< z0uX=z1kNJB&;OJ3|MUC*)6;(l(s$BLHo*V^2tWV=5P$##AOHafKmY;|7&3v+(<53i zBba8bS}NORxmYYvxy*0IH|kbdR?C&DX(*;%Hw)o54JgrlN&D7=`F^UTT2tWV=5P$##AOHafKmY;|7%GAJv?eCG zP?6>