From b5e3c47051de16730d71c3ddf6144a8f4d349615 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Thu, 7 Aug 2025 23:08:05 -0700 Subject: [PATCH] wishlist functionality --- .../wishlist_service.cpython-312.pyc | Bin 0 -> 11817 bytes core/wishlist_service.py | 274 ++++++ .../music_database.cpython-312.pyc | Bin 61848 -> 70930 bytes database/music_database.py | 169 ++++ ui/pages/__pycache__/artists.cpython-312.pyc | Bin 243020 -> 245155 bytes .../__pycache__/dashboard.cpython-312.pyc | Bin 96983 -> 133319 bytes ui/pages/__pycache__/sync.cpython-312.pyc | Bin 236882 -> 238788 bytes ui/pages/artists.py | 46 +- ui/pages/dashboard.py | 800 +++++++++++++++++- ui/pages/sync.py | 45 +- 10 files changed, 1321 insertions(+), 13 deletions(-) create mode 100644 core/__pycache__/wishlist_service.cpython-312.pyc create mode 100644 core/wishlist_service.py diff --git a/core/__pycache__/wishlist_service.cpython-312.pyc b/core/__pycache__/wishlist_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1536d48b5e7892e0d327d0cbff7037a1e11ed5e GIT binary patch literal 11817 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 literal 0 HcmV?d00001 diff --git a/core/wishlist_service.py b/core/wishlist_service.py new file mode 100644 index 00000000..2ee0d78c --- /dev/null +++ b/core/wishlist_service.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 + +""" +Wishlist Service - High-level service for managing failed download track wishlist +""" + +from typing import List, Dict, Any, Optional +from datetime import datetime +from database.music_database import get_database +from utils.logging_config import get_logger + +logger = get_logger("wishlist_service") + +class WishlistService: + """Service for managing the wishlist of failed download tracks""" + + def __init__(self, database_path: str = "database/music_library.db"): + self.database_path = database_path + self._database = None + + @property + def database(self): + """Get database instance (lazy loading)""" + if self._database is None: + self._database = get_database(self.database_path) + return self._database + + def add_failed_track_from_modal(self, track_info: Dict[str, Any], source_type: str = "unknown", + source_context: Dict[str, Any] = None) -> bool: + """ + Add a failed track from a download modal to the wishlist. + + Args: + track_info: Track info dictionary from modal's permanently_failed_tracks + source_type: Type of source ('playlist', 'album', 'manual') + source_context: Additional context (playlist name, album info, etc.) + """ + try: + # Extract Spotify track data from the track_info structure + spotify_track = self._extract_spotify_track_from_modal_info(track_info) + if not spotify_track: + logger.error(f"Could not extract Spotify track data from modal info") + return False + + # Get failure reason from track_info if available + failure_reason = track_info.get('failure_reason', 'Download failed') + + # Create source info + source_info = source_context or {} + source_info['original_modal_data'] = { + 'download_index': track_info.get('download_index'), + 'table_index': track_info.get('table_index'), + 'candidates': track_info.get('candidates', []) + } + + # Add to wishlist via database + return self.database.add_to_wishlist( + spotify_track_data=spotify_track, + failure_reason=failure_reason, + source_type=source_type, + source_info=source_info + ) + + except Exception as e: + logger.error(f"Error adding failed track to wishlist: {e}") + return False + + def add_spotify_track_to_wishlist(self, spotify_track_data: Dict[str, Any], failure_reason: str, + source_type: str = "manual", source_context: Dict[str, Any] = None) -> bool: + """ + Directly add a Spotify track to the wishlist. + + Args: + spotify_track_data: Full Spotify track data dictionary + failure_reason: Reason for the failure + source_type: Source type ('playlist', 'album', 'manual') + source_context: Additional context information + """ + return self.database.add_to_wishlist( + spotify_track_data=spotify_track_data, + failure_reason=failure_reason, + source_type=source_type, + source_info=source_context or {} + ) + + def get_wishlist_tracks_for_download(self, limit: Optional[int] = None) -> List[Dict[str, Any]]: + """ + Get wishlist tracks formatted for the download modal. + Returns tracks in a format similar to playlist tracks for compatibility. + """ + try: + wishlist_tracks = self.database.get_wishlist_tracks(limit=limit) + formatted_tracks = [] + + for wishlist_track in wishlist_tracks: + spotify_data = wishlist_track['spotify_data'] + + # Create a track object similar to what download modals expect + formatted_track = { + 'wishlist_id': wishlist_track['id'], + 'spotify_track_id': wishlist_track['spotify_track_id'], + 'spotify_data': spotify_data, + 'failure_reason': wishlist_track['failure_reason'], + 'retry_count': wishlist_track['retry_count'], + 'date_added': wishlist_track['date_added'], + 'last_attempted': wishlist_track['last_attempted'], + 'source_type': wishlist_track['source_type'], + 'source_info': wishlist_track['source_info'], + + # Format for modal compatibility (similar to Spotify Track objects) + 'id': spotify_data.get('id'), + 'name': spotify_data.get('name', 'Unknown Track'), + 'artists': spotify_data.get('artists', []), + 'album': spotify_data.get('album', {}), + 'duration_ms': spotify_data.get('duration_ms', 0), + 'preview_url': spotify_data.get('preview_url'), + 'external_urls': spotify_data.get('external_urls', {}), + 'popularity': spotify_data.get('popularity', 0) + } + + formatted_tracks.append(formatted_track) + + return formatted_tracks + + except Exception as e: + logger.error(f"Error getting wishlist tracks for download: {e}") + return [] + + def mark_track_download_result(self, spotify_track_id: str, success: bool, error_message: str = None) -> bool: + """ + Mark the result of a download attempt for a wishlist track. + + Args: + spotify_track_id: Spotify track ID + success: Whether the download was successful + error_message: Error message if failed + """ + return self.database.update_wishlist_retry(spotify_track_id, success, error_message) + + def remove_track_from_wishlist(self, spotify_track_id: str) -> bool: + """Remove a track from the wishlist (typically after successful download)""" + return self.database.remove_from_wishlist(spotify_track_id) + + def get_wishlist_count(self) -> int: + """Get the total number of tracks in the wishlist""" + return self.database.get_wishlist_count() + + def clear_wishlist(self) -> bool: + """Clear all tracks from the wishlist""" + return self.database.clear_wishlist() + + def get_wishlist_summary(self) -> Dict[str, Any]: + """Get a summary of the wishlist for dashboard display""" + try: + total_tracks = self.get_wishlist_count() + + if total_tracks == 0: + return { + 'total_tracks': 0, + 'by_source_type': {}, + 'recent_failures': [] + } + + # Get detailed breakdown + wishlist_tracks = self.database.get_wishlist_tracks() + + # Group by source type + by_source_type = {} + recent_failures = [] + + for track in wishlist_tracks: + source_type = track['source_type'] + by_source_type[source_type] = by_source_type.get(source_type, 0) + 1 + + # Keep track of recent failures (last 5) + if len(recent_failures) < 5: + spotify_data = track['spotify_data'] + recent_failures.append({ + 'name': spotify_data.get('name', 'Unknown Track'), + 'artist': spotify_data.get('artists', [{}])[0].get('name', 'Unknown Artist'), + 'failure_reason': track['failure_reason'], + 'retry_count': track['retry_count'], + 'date_added': track['date_added'] + }) + + return { + 'total_tracks': total_tracks, + 'by_source_type': by_source_type, + 'recent_failures': recent_failures + } + + except Exception as e: + logger.error(f"Error getting wishlist summary: {e}") + return { + 'total_tracks': 0, + 'by_source_type': {}, + 'recent_failures': [] + } + + def _extract_spotify_track_from_modal_info(self, track_info: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """ + Extract Spotify track data from modal track_info structure. + Handles different formats from sync.py and artists.py modals. + """ + try: + # Try to find Spotify track data in various locations within track_info + + # Check if we have direct Spotify track reference + if 'spotify_track' in track_info and track_info['spotify_track']: + spotify_track = track_info['spotify_track'] + + # Convert to dictionary if it's an object + if hasattr(spotify_track, '__dict__'): + return self._spotify_track_object_to_dict(spotify_track) + elif isinstance(spotify_track, dict): + return spotify_track + + # Check if we have slskd_result with embedded metadata + if 'slskd_result' in track_info and track_info['slskd_result']: + slskd_result = track_info['slskd_result'] + + # Look for Spotify metadata in the result + if hasattr(slskd_result, 'artist') and hasattr(slskd_result, 'title'): + # Reconstruct basic Spotify track structure + return { + 'id': f"reconstructed_{hash(f'{slskd_result.artist}_{slskd_result.title}')}", + 'name': getattr(slskd_result, 'title', 'Unknown Track'), + 'artists': [{'name': getattr(slskd_result, 'artist', 'Unknown Artist')}], + 'album': {'name': getattr(slskd_result, 'album', 'Unknown Album')}, + 'duration_ms': 0, # Unknown + 'reconstructed': True # Mark as reconstructed data + } + + # If no Spotify data found, try to reconstruct from available info + logger.warning("Could not find Spotify track data in modal info, attempting reconstruction") + return None + + except Exception as e: + logger.error(f"Error extracting Spotify track from modal info: {e}") + return None + + def _spotify_track_object_to_dict(self, spotify_track) -> Dict[str, Any]: + """Convert a Spotify track object to a dictionary""" + try: + return { + 'id': getattr(spotify_track, 'id', None), + 'name': getattr(spotify_track, 'name', 'Unknown Track'), + 'artists': [ + {'name': artist.name if hasattr(artist, 'name') else str(artist)} + for artist in getattr(spotify_track, 'artists', []) + ], + 'album': { + 'name': getattr(spotify_track.album, 'name', 'Unknown Album') + if hasattr(spotify_track, 'album') and spotify_track.album + else 'Unknown Album' + }, + 'duration_ms': getattr(spotify_track, 'duration_ms', 0), + 'preview_url': getattr(spotify_track, 'preview_url', None), + 'external_urls': getattr(spotify_track, 'external_urls', {}), + 'popularity': getattr(spotify_track, 'popularity', 0) + } + except Exception as e: + logger.error(f"Error converting Spotify track object to dict: {e}") + return {} + +# Global singleton instance +_wishlist_service = None + +def get_wishlist_service() -> WishlistService: + """Get the global wishlist service instance""" + global _wishlist_service + if _wishlist_service is None: + _wishlist_service = WishlistService() + return _wishlist_service \ No newline at end of file diff --git a/database/__pycache__/music_database.cpython-312.pyc b/database/__pycache__/music_database.cpython-312.pyc index 234de155d03182a8378c8b5499ef6c341d67cefe..834246cf6f75d5c6cefcdfb7d456584a68481532 100644 GIT binary patch delta 7519 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 Uuc;b!C}V*lGP1J-YXATM delta 808 zcmYL`Z%CC_9LLZ1Jm)<3-sgI)T^|EW)2y3a9h5eg)w*V07nQ{Z)#%nB>A5zr2rmO| z>#r$`tjtdKWX=9aNq=Nozv`B6w$+4zNM?;mMI#G>D5x#Otr$JGH=P&X7az{=bH4n} z!SmwCV{J^I&u1{_ocp5Xo2s|$;+XYr^RmX6&7v#H>M>@cOuaT1IV9vA z8t*8)Q4yV0$=_?Z6{;7JqRoNe=QNPCU+^NXDn6b<^&izh$zaImy?{hd#=tiFRA)+l zAZd$sjJ5F(ypCDEt=cxN1~sf!Tg7S+OeSuA;#SWl-o-rib7m&(&DsFZe~xAcdnpzx zWLK+(?4bHRyJB4F^wI)$v%nKh({9d93!B<+rVOX4;>qo3rQyj7G#sZu&NP9mv@EaO zg?>6#P$~JpjI^SwE|}C)d`F^;_Lq&i5TzfM{-mLWKK(K*aGJhf@tMR8x)w2YJfZ%z z>m`z?u(C-ak9w;N7k;FH*hZj&Lf;+`I7PQ=pXrEG&-SSv{+iS9?sgsd)X?-DFy&Hn zoj^J5YWZKFn(B^P!fE*Q_-!3sbp1?&2NS5cs}p!g&$}Dlm_fDItK3*kPk!yvaDoo? z4oO_3jeQvs_o;E9ne!1r6YmU4B-5**HyT3p;=v%t68hz@ERL_#qeuT>jrwDF5?=?k zlguk`@K!^sJt`3L9(6t@Hr-iitko`&*yr$D5-a#x8K0`78ECh!xsZj~@evmSz)5?K z2b1`HVSnvGj$RJw|9ee*j|Xi6?)c+mECo93{IMw1J01058Q(A#1n>F8J0cMscCH`k zxL_~zW2WBipcB&G;m0MMvS+3s8)xm6DQF6GBrcCnG@IbWJyE`CT~+M8COrWY_E@)V bn8-knJ)<5?C diff --git a/database/music_database.py b/database/music_database.py index 7c05a4e8..b991bd3a 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -135,10 +135,27 @@ class MusicDatabase: ) """) + # Wishlist table for storing failed download tracks for retry + cursor.execute(""" + CREATE TABLE IF NOT EXISTS wishlist_tracks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + spotify_track_id TEXT UNIQUE NOT NULL, + spotify_data TEXT NOT NULL, -- JSON of full Spotify track data + failure_reason TEXT, + retry_count INTEGER DEFAULT 0, + last_attempted TIMESTAMP, + date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + source_type TEXT DEFAULT 'unknown', -- 'playlist', 'album', 'manual' + source_info TEXT -- JSON of source context (playlist name, album info, etc.) + ) + """) + # Create indexes for performance cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_artist_id ON albums (artist_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_album_id ON tracks (album_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_artist_id ON tracks (artist_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_wishlist_spotify_id ON wishlist_tracks (spotify_track_id)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_wishlist_date_added ON wishlist_tracks (date_added)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_artists_name ON artists (name)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_albums_title ON albums (title)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_tracks_title ON tracks (title)") @@ -1372,6 +1389,158 @@ class MusicDatabase: def get_last_full_refresh(self) -> Optional[str]: """Get the date of the last full refresh""" return self.get_metadata('last_full_refresh') + + # Wishlist management methods + + def add_to_wishlist(self, spotify_track_data: Dict[str, Any], failure_reason: str = "Download failed", + source_type: str = "unknown", source_info: Dict[str, Any] = None) -> bool: + """Add a failed track to the wishlist for retry""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + + # Use Spotify track ID as unique identifier + track_id = spotify_track_data.get('id') + if not track_id: + logger.error("Cannot add track to wishlist: missing Spotify track ID") + return False + + # Convert data to JSON strings + spotify_json = json.dumps(spotify_track_data) + source_json = json.dumps(source_info or {}) + + cursor.execute(""" + INSERT OR REPLACE INTO wishlist_tracks + (spotify_track_id, spotify_data, failure_reason, source_type, source_info, date_added) + VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + """, (track_id, spotify_json, failure_reason, source_type, source_json)) + + conn.commit() + + track_name = spotify_track_data.get('name', 'Unknown Track') + artist_name = spotify_track_data.get('artists', [{}])[0].get('name', 'Unknown Artist') + logger.info(f"Added track to wishlist: '{track_name}' by {artist_name}") + return True + + except Exception as e: + logger.error(f"Error adding track to wishlist: {e}") + return False + + def remove_from_wishlist(self, spotify_track_id: str) -> bool: + """Remove a track from the wishlist (typically after successful download)""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM wishlist_tracks WHERE spotify_track_id = ?", (spotify_track_id,)) + conn.commit() + + if cursor.rowcount > 0: + logger.info(f"Removed track from wishlist: {spotify_track_id}") + return True + else: + logger.debug(f"Track not found in wishlist: {spotify_track_id}") + return False + + except Exception as e: + logger.error(f"Error removing track from wishlist: {e}") + return False + + def get_wishlist_tracks(self, limit: Optional[int] = None) -> List[Dict[str, Any]]: + """Get all tracks in the wishlist, ordered by date added (oldest first for retry priority)""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + + query = """ + SELECT id, spotify_track_id, spotify_data, failure_reason, retry_count, + last_attempted, date_added, source_type, source_info + FROM wishlist_tracks + ORDER BY date_added ASC + """ + + if limit: + query += f" LIMIT {limit}" + + cursor.execute(query) + rows = cursor.fetchall() + + wishlist_tracks = [] + for row in rows: + try: + spotify_data = json.loads(row['spotify_data']) + source_info = json.loads(row['source_info']) if row['source_info'] else {} + + wishlist_tracks.append({ + 'id': row['id'], + 'spotify_track_id': row['spotify_track_id'], + 'spotify_data': spotify_data, + 'failure_reason': row['failure_reason'], + 'retry_count': row['retry_count'], + 'last_attempted': row['last_attempted'], + 'date_added': row['date_added'], + 'source_type': row['source_type'], + 'source_info': source_info + }) + except json.JSONDecodeError as e: + logger.error(f"Error parsing wishlist track data: {e}") + continue + + return wishlist_tracks + + except Exception as e: + logger.error(f"Error getting wishlist tracks: {e}") + return [] + + def update_wishlist_retry(self, spotify_track_id: str, success: bool, error_message: str = None) -> bool: + """Update retry count and status for a wishlist track""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + + if success: + # Remove from wishlist on success + cursor.execute("DELETE FROM wishlist_tracks WHERE spotify_track_id = ?", (spotify_track_id,)) + else: + # Increment retry count and update failure reason + cursor.execute(""" + UPDATE wishlist_tracks + SET retry_count = retry_count + 1, + last_attempted = CURRENT_TIMESTAMP, + failure_reason = COALESCE(?, failure_reason) + WHERE spotify_track_id = ? + """, (error_message, spotify_track_id)) + + conn.commit() + return cursor.rowcount > 0 + + except Exception as e: + logger.error(f"Error updating wishlist retry status: {e}") + return False + + def get_wishlist_count(self) -> int: + """Get the total number of tracks in the wishlist""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) FROM wishlist_tracks") + result = cursor.fetchone() + return result[0] if result else 0 + except Exception as e: + logger.error(f"Error getting wishlist count: {e}") + return 0 + + def clear_wishlist(self) -> bool: + """Clear all tracks from the wishlist""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM wishlist_tracks") + conn.commit() + logger.info(f"Cleared {cursor.rowcount} tracks from wishlist") + return True + except Exception as e: + logger.error(f"Error clearing wishlist: {e}") + return False def get_database_info(self) -> Dict[str, Any]: """Get comprehensive database information""" diff --git a/ui/pages/__pycache__/artists.cpython-312.pyc b/ui/pages/__pycache__/artists.cpython-312.pyc index d278e664f91caf46b8593b47f23af99250ecb80a..490dfbc0b4c741f8bf7b623e395f0d5b902d5f83 100644 GIT binary patch delta 31733 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 z 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�?+OLxYTNcX@TaS$?lBGm_=H$X3y{DAD5 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 diff --git a/ui/pages/__pycache__/dashboard.cpython-312.pyc b/ui/pages/__pycache__/dashboard.cpython-312.pyc index 7c5157974de92e569917655c8d8b433239f4a1f5..8d0e89d9ce7902b44875e6e48412553bb62b86c7 100644 GIT binary patch delta 42196 zcmdRXd3;pWz4)9v+nwYlnIw~ao9vnFBtX~%va*LIVT(WrnE?V>@XkcY6d1G{_EiOs zzKg9^T-su_Ha6{pwzi_!>T9)~AwwsQw#C->TH4wo+SW(kd;Y%PbMBp)1oXXrKJTC3 zpeN^^efuuwd%ov;LVq#7d{RvONkT%L4t|65fxTb+(aFS2a`9yOGT)H3$2yeUlRT8t zlQNXrlR9MUu@Rk9=M)ChhSGb|hcbFHhBA9Hhq8LIhO&FIhjMyyhH`sySsMLd-cWu| z{!l?r!H~Vj&cY0Xg*}Ck-!NFzQ^fuj_Y}il<6y~9X;0};Sx*_!S+EAfV0n+P+-d5m zU>}t|mGD;_tQxBBsUE87sTr#6sbxi42J43Edg_=zZm@o6R?jT8s)n8h$P+)M5LzA+x|)SnK&Zn-BGs*7p_LF? z6%*RYLaQOPCT4`zjv0z|E=P$@Hj!MN)7(?BRd<4Xo*dL2(r?urB#zoESii$Y9~8#h z!{+sE1AT)dd&1KC?!H}vuFV6^JuXk!8vC`v;~EObuV3!!bGqbB1FnPcvBu?g_w8}D zj2v>9=_-9?X4tZR!|3oZmI2Y-du2$yZe(PTj_Th?vq71?2M64H2M64qUbjoWeW2e( z*BHv;)XCDlH%yNhGJG&;?AK#FuG>wV!l%R@lT-gGU60vm09SMx!L>LOoTg8ao;ar& zzT%xCxRTQXZh|uo+(c(QxJgb4TIoAxh-2=mWHmeQl zmizkmyVs03`_>M6SK{A6`|c6h?o^XQQoDzxvG0~ghU}ibF1ywl@Obvx2M2C<*$>Dg zdt?|Eo)$9oo9XyxY__AY&>tX*&ZG8Cr@PBchrU=y|Kl?W_n8uXhYg2yy+$qauo1pY zkuMW`nYAxZvgXsq31Tf;*d85xF)V~3P6A)?TAWjWFKOCWg8Jp-NfRTHm@#SEmv!1# zq!Fx&6fKTbpE~W!rhN@y)6%rB!@7MnnqQ|={+>=!ly}lep-<<~cgnaTIvkl{qkHs# zOAd>@y#vDop5ETDRU30{0fnsx`efIzr`Or%-n(n0Pj-eAwK;S59)RT;PIMm_@eJ%9 z>+K&LfRy3*1B0$Z+DDRmWOUH&a_v_Ge6j=T4kr!udHVOl{Pntq_Y4fX!by>e`6>uY zo)J&qV6XaR)9QqV_G*hDZ0+wG?spCL%C6f+U2c!d8P4kK_rN-VhRJ<{gAk?VbBB2m zK3JN)kb2YwB{^M(!m0fuL(pKz9GliX+TRb$Z1-qv3D(_x1B27jS=H*IwPe9~4)qPg zI1G;Us_E4}WMZRytW|DTpWMHfcMb+2EV^Bu(F47s17RD&P85uJR)-iixLt$00KW5$Lsj9rc^beX}9fTh;bI zLF!`QVuDO)=pvb3n}WXE}mm<&?etxFIO!MgHbj<9E)K_BW>tzcZ%_ zYVmLG`zD>;*Em@)n|)n{uOjA~%FV;SSyLqr{98C>FU7xkQ;tgfTQ*g~z6+;{OYqws z`L-9bbWl=Zv6`cb<$w?e^Oar}rF#?ZO89)zlqiM7!hl&Uyd)OCr=x5BD@!O7=`)|q zrUxD>F*x;43H0(a`P4d|K!1C$O-~LJT9}xYYIL%HPZ?kY;74>2-ghZY`IAT_n)9W& z1hYnHj|hha<&F_zvqtfVQ*>H9R)q|aJVkFHsr10fR7DU;Ivt*hr`cbMC-L;yvsKjo zTAl&YN_6MBH2T`JHhTIs8-4!Sc*&E+>(%L=($T4t4*KF3Wy`!o0JoKr4UmE?dZ^lECD*9n|e7dJJS_bz4wn(Mp1NqAQbdp8`r;Av9 zw|pnw?x~uY_K4mS!6r@{{noiEGK)U?S}J4|zf(y2ziXpQzMBd-cHdk|wiVE{@9@@t z<6H41F*!4|VS>7IwK^cvJldR)%1ca2)6&2wt1+?VFTnB%%Yf~z%Zw(EsVGYeg^KcH zDq8fB6|tDuiWakqcra9yl&uvB?b7dSR^Ck~xd!mHD(|L|G_nYi=jkJ#^QL{$pPWmj zmmf_at#o}+s$!i*;thw1npL^SM)LKMuRq3oH9~MZW}%7K<9%Z}j$H5JF2od!JPt7; zx^acQ!aV>eY+(0*%W1#eCA$YkhV8rh-0%e+_tqv z0&%M0jiYbS5?d_KwqU^kaBcQa&|^AfoRI{pI6b^y&o@t$!gC{w4_x_4L_4+vuO47fto-M}Pi$K54+f zn>5Y$M2>#mfkV{)=OX&&$K&WLPju5)e^3$ALDM|;!%oD7O|6Vp_pO6!7n=DfETk`7 zSRAd^PJj2h#OuRxnNfO~aZWw_qi;Qyl{cEsx~81`^SOKNt%E>KBdbfTz}3(&t8w-} zR)J~v?%7Rqny|gaosME)Y+Rnrz$J_v>>ls{%Z=Y_fKeJ69qJnJy6AKFFYuWmtOKS^ zMl=&PZDx}fHnhOxh2sW>caQWA_U&>F%E+{Z&1`fHL%XwGKz}5Bd2iD5mKQcTF)r6DWo<12E zLaYSYDR($d<-;(4^2l(n`l;ni>Bkw0GBvE4>0#lxKBp5q1pNtHz_0F+(7U>CY-H5q zFvu&h&89*28J70qR>>#x@eL>J8U@H3hA&L)MoNv?OJ0qgT@KFwir;}zVr*C&WN^<0 zgm_E`%tJ`b35YrV+_^!qiLns)d8+kvYEVosWdYd%G5e~{kSo2fGguPdm*}fMYBVgJ z%xI>A|2bz#PNBc>mdWhJ#|?}Hxi{&qq>xw;5DT6#p5Ajtp0qav#YQNHN0bJ{Qh!-f zP@D(6Qf|c;7gOJvG#^1;3!9P&tXD{E2#5_A8+(J|&X_<)Ky*xsRUxrCAU2;b35v^N zBFh3|*;QRi1vD`QnpmkTs(m~k8Kr!dC_f$D;Bu`$CwoQ9HdB%-4j60!6%hET=gK*i#q zxFiybLwQvvl|!i#Yk6_4zqVttXxTs9WM6%2puROIw%yQX3EK>%U(MB7(nF#>Alids z(Uh3Rr}(rXWN!-CaXMqdzMAxC5*y_kQvus*f#reId@2_iUojEBlzYv@Xnv&N!GfUZ zpl_ZjY^n=n)v+Ql8WZPT63gW^uyFfTp$~X`BgcQ z1dmx+nLzUCx;-N9kPXUP+ep5}N%jMYked$ENB5*Dy%oex$4_KXyDOEB{6W%>86pvB z#HcjyAnCR*>YS#B^)Wn>ZeNO0m_{7R@D5;kFFzV5crqe&rL^g`zo*k}11fxk0M87G zku#&8$VrQL5>F1c9DX$Ek&JYw*(pXuMrzld($mfl^D~^5JvuGwi2kr%PCKl3k}Udz zyI8h3EpC`N0^UrW?yxaZ zLZpQ}j&ZS(Y8N;YquHHFkxV#4@FTj<|HYs~;gZMB0>A+w(~2V|PZ53V)?CqPJ#6}- z?qPuz{R7V=rsBrn=VoCyiM zX`LflEU*w-Tu0}1QwMNVPM_Ude3N)PN~Xq`fCo^zJQg+#_YJwOCu|(vKMZ7pvz%cw zkP%?f+}9JeaL%(`!7&WSvz)#C$hcjBg`&HHqZBr;a}6V7=q+fMsAM?YoMqC2g|hmT=Qi9 zV#sjQ^e^epubN!CZnAED(7FLKC8yo2t(M8Um6xom-p#{aeek|rXU)2{S0}*CyeXyJ zyZWxx{+tEpW}PecXDtp&OQvFn(qFddxvulc-`jL;J;As!8X9J~B4{2^vT%eP#j#_5 zV^Ff_Erm_0?3pGR2U%tW_p0cYq36!-4sSNca>$hXJctN2?%%z@9yYQ_Z%zl3T-rS& zcE-HNrg3C)?6w#hEiXX_F`IlBI{VPs4^9~I+?u{axc6bw%1wQPqb_+V#y6wWf{w2h z-8OXE(dj^E893oQzGoN)#M!$GMZdixyCZnkE#Hd?PoQ%W9o*f8#eR9fGtl2RDBlC0 z4uN4q`2p~|ks4???g03`4IIb$KIYhAu$DI=GCEhWqzo0mRSAE0OEv30XD3?B-MZL zc2edwKnz==h+)on-*}flYkp8#@TQazlI#J=e%f~Wpub_wtNs2BT|e3V$`*h9rjWEb zAZ=!tVGCe}JQ$)olUfUO_q9skUMR4%)#xu&ENrXLzf@rae?QKI_A~3~Gk-G+vY^~H zhh&R~$$p5H|BXJoHeK;=1|jG5Q+Xy9c7d*cE1hm$n{GnM<`Dst2uJkDNoLUVmu0sRZPkX(pJoys4;68afm4Ex03W!FBH%(^lGic7&@ygEzYS|V+h^0 zCtqu3@w8@&teNEV7p8@p_HTm@6)L2Qq+6T@#v>;mrp+B`h%@4S#xeNkFnJesF#yIR zEL`v)i-QGkAK4v+Iy;K+fU$xUu&eGLmGOuLPPm2!Jo2ZY#iMxsbM=w?@3wcblL7#1 z5PZpZKq!ZMhb0E~K8wMN@FbM6AoAz&o3R;o^`N0|IC{FlPaS%h%+M9if;}@}mnbX( z!^O`-v<-LSP#>J;L|Q3RC|#3eup<<0v+Yy4jxRt0@6!JT*YUA9AqgPUoxs48aT7Ix zq?+->i%E+am{tUmDxPjU;|bNZ2I^WTtJ*FmwfUy9@*|m+_AjGN9WN*~B;n05m1A1i(V?G-%KQ+ad#On^@+Qm~hxC8QiId z>3Jn1+iBEN6Wy5Uupah#=EDY{W_n64*>EFih8>boO>T{D)i62iE^$AZ0_Uydu%I2E z95Ed>(RH7&(R=o!F-E}ur9#3&6w45tC?IbDxPk8()9BKZx$HzkghPg({87puGr{&K z8`c>60eQaVh}jc4O`ylVV@q{f4x6WwY1SE^r%=mwIC5eW?H&+i(-HGg^O%YMjUmc$ z-iEHIjGY6O9e^qV(QK=u?T$1G4FJi(RXzf~u+HaA(PZI_p||6vs54CL-fR$#4v%>3 z{jhKHxKyaM!-1VV#`ifgr`t&0c~*^#!FDEDz8j0r1V~o*37YtipFhGWuSXs@5YF|n zK7jfG*AdVlbn}S3A9ft<2+`~Yp>iLH98$Bh_@Ab>-%`c1mF<(vFt};%Ob={OBhbNYRI&-kjv|OLhE(}*6r~3 z?hURRI8~|c8#au$O=izOR~X1%6v|#2$Xk$ z%xXNo@~zZ7wlAm&SZmG<`xkErTDxE~1js0}_{7lZS*LqWulCz!g=|d$ThpX%{_*zL zQ!}UK=m}bHg&ZI-Om7%32{m*C8aiGvzT)-YIxv~G?|8=)Y!Eu{@^vDUBGrVXIRRUMO;a`=Zp61%F;D zv$R+1U#e?tFW3LD+z9@DhU?5Wj_Xih=@=shI=r`c@Yto&2|_BBTvWRIpiT|-v>7OA;>!;iXkl2|2_*%!d1q87z*Mc zI19Cyycvw+u+aZLQ2>p^b3wZQVHAGFD<`%S&L!lDGtgJj5#x09X1j(K zXbk6Rd~Db8Abuk&F#}CFOdLae9iqbV5qtxEVmL#Lbdg~LRPte{K*a@5!nb!Z;DV1q z1wK4it^F7@2dZ{D_d8G$N=DKx?Ig;03F?U?w=NA@Ir|aIZ7|lLb{pVb>R1`no$4`g~_=R%@>ALawFFtiMp2*(T^;5{%$i!8HB<7EJS_V499#dRPEJjclX; zKLgZU6;L^CoB^crH(_u&T%yF|*AIW*&NuBHPYUw4AS9dsL>i=%LtHBmpcD8DZ{bIQ zCsr~lyZ#rjDSsPlX1n$%5dLpq&?h4h%HKr?floe*&Q0O&35fA7{CK$g2(aU$wFP1B z&rk}7xfX;upUUn3LqPXR?&LhAdDCGo3UXWgdq)}6-OiZ)!i!Sj>GeSTE~)$f7^w68 zUw~Z<`CN2v{kc|u)>0sqj8X#BA*JL{m))ADyO3vT6ZIF$GTZd}m-I#>e4u1*tu6l{ z7#`dSFgX~E691ua38p4dtVl4WCYVXBh-XD^8{q_BT132+MVKlSRk?q zO0(eh`3M;Xp57V9{T>S~_(QG;(yw@sc)`x#wFYXvGd{92E=T~H7g1samc8hR;K2Q7Z-$c!jt)ek#(yQ>lzToOTTL z!RbiM0SVg=>>l+11VSvFfYf@6o1vwQI=N%2jiK6uV};%np9UjtP+8#{ac}8Iz_?rM zm`Z~snaS{?LO2G_OqdprstBN%$j%w8>d^G=24x2KUO3w~xIpp{Hud%Q``|2MCdJkB zg9x*Rc^HHv5{|IgUxE+sBCY8kbI|aS(gqpf``=NW8v#7S(m|#wYEZOQ3e+lV!SQ8N zQmQ|Tv8Ag6jjKU!^~wf+#=3yCjzyiyJY5nhY6=uJ`3vWr6Z{zq1JXh@6FU-U4OF%I zE8EVu`7>4oq!q`PF`+_9AgSc^Z5NX&rmPwMoV5Y#THi_YW%MHjK4E68FFT89^H(lD zZw^WtF)s{Ag`6u6N)?Qt-n;6qRd=u9Trq+6M}+cA&nKVP`?K0YQhPvZXT-Ea1M8N& z7Q606yQMW=|Dq$a)ub0Mn2hkj1@6L5;l9-+W`HYXX_XzRBwx8Zfus`dr%zu_P4)tV z8rfL`w8tbfGYOR^NAq+Y zz{^?~j9d8M7#NT9=5u8p1Zovjm^Qe!v-T$Pcn!$Cw>ATSfyM`bk?J|v2a+_9tug%) zh8?gV-+uRvdgV*v;=1<^xb3LK3z~AEwSs%_T`oHuY>o`OYQ0vi9B37QpaH@C}_7ZO_;ELtIHi0I!74)?UOyw5c&J)`~aQj(K!!J*nF^0 z9)_A{K)w7$Ou_c#D*PK|*MY$?PT!|NKNjts^|8?JV;cXptd|i*pt}*~uR~@8d8=hJ zdDB)9vQ-3Z0FE|)dfAk96YDFNmB9!WD3Zym#Q|Gf9ssgsEGj0Bc zHh;&#pzY94WzZ@$?O3?9gcK@NM?^`f(uN^Mca+88Y_OuP*NLuix;KO|LBX*KZ6-n*!1%wh`V8 zcpx7J;LcV(%GXU9=-w!0C-k;Ho3tAeDk_DiYuDy0b-5&$`rD1lU8N-7Cph$8n?@p; zuAQChM0Iz9D0c~Dj?R48B)j&3F4vQUp>{V+gihTd$>8BQmmRxHg9fGmq+B!p#aT&R`oa7m|koOiR8*8qWWgA<_ zwRxGwc5-cYCi-V*89T_exhclY;^>tE2y9a++^3)bUxEH zr6K_?73p1ofFS|=)Y~`)b{hw8;F#I18WC>QnB=cO0`Dv?zO!>_7vealx{tN@*^$oy z8Gwc%IQHz<=@3b%KZE-By?Qw4?E6frPZNFinBcY;GzIQR!R`%zJ?1gu5U!VeoWQWQ zi;D_79ioi70`hC<{1hFW$FP0?v?39I$Y;>`8aPkt_~5L@c=IB)067Sscuwj@y&ss4 zyz(>pGhLH;v;1QA>)9ozyUr9(X4l@8#50E=QNIyy{g6-57mRC?X58&j-l_t*3kas+ zG&XvzXH54b!Im~cZkesF1?c+*I&AO4wlY|@{8ex?DZ|YYu{9MDEF9wt3yXx4TNDy& z0@iIF4T`vgHFBHc8`#(Rkl};qZHl0ntzD6rO3Mbdx45Zn03lEq)GktFPsFI>ohi%RnK3{YlLh4)9M14MrKvP7OOVp#wx>x!I6;z^1ontj05*Y)*wgS zVZA&$?6pr@gEN;Pq&SPK@9$6wXS7$5f0O1;71o6cI|79rlZDIuQa;F$Z8@P-Mf(eoxWW8LZ!cR=cKLg zJ-twnaw7g+QD-Yan!5Vr>X5B5U~3H7<^^o?&e=lqR|n>=er5k9ThC=JzCK{95837f zY)nGEA~1J_f0Z+6a{-f`oTkK`jC(-(;DiA@KJfjW9d#4SDgqKP)PVt=AW#Y7a&A0# zZw}NpztnjCwhMDZ(#n9elCj^bfb0Si)ZMwHO{aT7Xvu7?(p|{5!pDWGl-67H7jDsm z|0SKJUDCf~ncr^K|Ilm%|C5$jOy|Ry_!&Bgw`7Jp7-sn$e)`b~fD=o85RWrteg-E2 z4@VgJS@^y|FxH(Ye_scG-WN^AvcFohjH?Joc$f(=dGv?#4!;Snp;f4qH-Q(#uTjLt z+J)$a-w$)3Zy`7|*?i|5E@c0pdF^d^I@<=4f$ zdz-=@+eDpZ%4C#}K|IZ{7A9dC zVKYEmpT{H1^XW2cZEGxW!0wc33$jxnE)X+2A@#tZn}GvoWFjEJ(Q-JAlH@e{3+sHr zAkfU@^QLQ1jeH0FWpZg*WDvqh2;{Km=;c+oxfaUv*rE(&$1;-99ZpoEdQnx*<&-~* zk)pPNmJ6|1EIVv+kM6=_{2~ltodRBqiMjuRAt=Iqha552o0N6CCuvr$PBxI0OAq~rw+SZPIvF<0Z=}4_3j$$MMTQh3${FLJRl>P z|4;Bc_%=cQ5d1gUBp9H%-fFCwwq#}qbA^dpGaoRRuxj}j{cCQYk)4v#?RjH4SR7lt zcpHJ;l+-3>A$>P5ogATJelM9%_vRO8LPJLrAYD})7v{-XM_FSgGxp-G^j3bHFK8lZ1**XWR2Ud2B6 za$w*YHjWO%?pgjaR)A0;vyov-%79nV`2|%6R>qcz6f;dYT;U|wAxH)!moBbdUi2+2 zf(?@dzSueAW90yW=Y|~mOo2pyR%;R-5hg;lrJ~S9=zq`cF2HJ|YR1jj=57ZG8=bCQ zqkEvUw>5Q$Y{wyI6Z(8pi5bj5=;S;?f8SJYE@06YCpzhD zlvq#ylkS+GXF7r{o=5MPpPGzlRxZWlU?Kt+0{!y*3TY*VvSm4o(AVb|npuI{Kn$~B zzEDT#q6Mv{Moj+z{q%xrGjhjrrPfUoeSLu>TofjLyI_UM% zD)ykCO@sxKMJ9sJf(s&5XuhX4L$K>7zS{aFeX`m-$hZJ=FC=u+vSOT@Pb~Y4j{y^J zWE^iK9;Ndm!=>nJMyCZGwuCtTQ85o&e!T}q;Fh<{<^p*6XuA>OpxxlKq0^2|bRmhX z{fWE_y#0F!y=VCXVS|3+$IEkttvFB}VW20w``|$GN&{r*e$vDiJ0Ck%4 zO+F5Uw~pb;wVm0Z88@-Nvr;dyIc0?Xp9y_*eZ^iJzpw!ZMaBUPn}I(C{IEB=ShZ1N ztB(!KgP1UG^Z--&9+d~>QxL$1jZHQmtk|(?#idvyP(9tb;Xi$`91!%EGeFtsV`Fp) zd|~5mDBUAlSo3y)feJYeD>0#S5|bLy$9Sc`;U{C5kU+?YjAcZJGD8@Z=)<)p??DH~ zv|3#zt@wRY9_tTS1Ui$1R(C&WTEo8SPrBiz30<)oSUe5q1;jm^o;%9d5 z9(rP9+PufGt}-mJfF;5WsYIi2B+T;^* z34MA~+3Hs@*K6qf6dl}k$vAd0a)$EL=zJX=R@pP?n^Di#Fbth<>uLVxMqba?cs&O; zd&y(;51X@me}*DKS&x}gU^gjb*89|%Yy%LePDjGb?mvrlu->y)eG@;~EMu{}xf2-n zEp%8*XEZW?9AnUVPEYUIatoh~@rl5eE(3AU((Tm+5xy1f6>^&;s-*}wvvEK_u|3zs z#ll7A`F5Kvk7~*_*71J0d4n zau-x|-B@nlHD(X%J4X(7!Z`jA!osP{a0c$F@d|Y>-0}pT`gte`5-_yRC<$V%m@cmC z-z@x1PoL~xo{vz4S8AA%8~MvnB!^7|OArp7*ln9Ac9xSe)?thMA|_y|7#P2RpI?b) zq6b{LRapCzMqWE!dzn$H+X(S?{K>27S=Tb*zxDU+{v0fi#lO#*cw)ERfRO#8eKi?C zm35AEK4}bNGm7yRjoZJtm)YHNJxmIip$Kp8e@^9#={dqZT zU|8}9eh3?I{D5)r@6Wd|{PQxTrvX<1O&?4YOooZF!3upsq&Qr!^wyEwtOuZi>C069 zSIFsg005)|;F#&xMl|h zstprIe}xPR5>uvoqh-Dv8E z6%I;*VAFt~MLOcxv;eD#_K?D6)JT@yZdnBRP}njAr!VqY@6awe4$OBPtl%yA6HPo+ zCA1kP79RSFQ2-wPH%B%Iuj^>j(YzEkkT}O|#UBF%KzASQ%=kLKfFOt z`pQ_tU`M&s3To~_U!AbuFkw8_PYk%4mD=Sbed5kL@(7si$lhRA5M zKrTV26rD0~G=zYC37cT#;M!djU~u&90)6MsTH$jBn)k_3hJHDC(5F8+t4Ku$zk=NI zFL5|x);MN%2=v2GHu~UG~UKlj|0+Tb~_#ODdSwpb9$4wLc-ltN0S1|I==)8^&;~ic@pC6qd zI$?BJUl0b^a+cpg-=EN_#xb*_<3Q)8ti!)xLUdk2-1h;r9KN1) z{md>?IhOGTed;qihFQlAG4dBa8Qd6>BNm~|#Y{iJI#xl^k-?6oKiORzrFDb+klXi) zNR7XMvye~k2-@gBeKt`=JQJV&l~Dc|lqP=<%Y6==@1s)-b%9jqZ|KJc$#VtzozE5d z+`nNQf;Xr471(lyL~#A7HxuPaESF(b#tr=z zlOtYXsQAX)h{)w-dDCv}AxI(~F-$!2PaDWqHkdcCVK-YXtVwJcFz)fD^T}oj9mPOb z-d)Xkq;i2Cx%;2Ub!xfiw2#$ijG4S8kQ#K1(cVKZn%Q87P+Er>QMv`kVlg_HHfBD} z&_5s#@Qa<%WH^@fw8F7$WFnu#iL8ah9~`5f2`14+_vUlJG@w%dgy-I0>B(ag-87!~ z*5YWcLnjNzdOiBs&T1q0uHY%W+=V{ep}}kqVE}8@4)j4iI=L5}o#>!!40u7-HExum z4}e+ngWv~dSlaI6H;L~`5$jC=g9L+sd zDg41em!E0}h}ikNEc(?`)X7RQMdv4MgTqQ>72(8Eb|Qa?7A<5jil}2#59Y{5M%}J; zuqSZ0-wskV6|cfgT>QETv)C!nFFlwmTs6?=9&BgCBvA7GVrJ-!st7UvTmTyhxxge&J6`Stn!hP8TM|QZ=H( z0;2c^mqX0rq*uRKApF%plOJ-x(7|!iKI z&`&&GY;MO2H|pq@A8!=egvY~=3r@BIZ?^c5val5>W2?dm8m~WNAxLsbj!J+rS^spS&}y9c%+p4) zgEboGU2cF9K4OJM7gvnof>G|)a(avvJOx-Zo?kpfw*@nB}%odl{#Qop+nIuKA*=B)`JeMuu!n%@( z{pr%t2cFw3v_yp>@dPe-6VLqUH->mN?QAdp&54J8(yI53;H2G-&Ovkzp#%S; zEN=|`Y`Nk3l8@r251nJ^+<^|Zk?~m|;bINtMdR}RCyf6*I(LIJ6L*J`xG;|H_*uOm znkG*C%#QoxXZ^3vVZbpAd9EN8WQ~!LxdsUR1pEodt9Klk<%GMjsj$zZ-wc#+o;Zsp zT+E_>3Ct2QO|;@-XMbd+JEWOtbjJE-SoIQC`Y86`eyB};7@eCg_k9?HPM49sezB7; z_kG9_WYUgFmz9w-M%Ng=qpg)^j-;A&z4?;$b7dVY2O;7%@ya4SBW((yedL&q> zvEfMVUkJWxNPZ7n&d59??lKUU*MD3ie-i7AAYWNUx6p(PPU#s=Rp~o@AY1~witmQ) zzUWHtz&7?_Df#H^LdT0?%h5q}8jfp5Gh!Ri2 zk@F3}Tj2F>y7i4hF2e>=f5#hHLX~Oap*Jp*mC-ph&zw^>iP4F@>C`=fZAPceM1S;) zHO;Iz{6=aK`NgTe_wc^ZG`*Wm%-Sval%+@S+yqu{$;5_VQbIP#zxS8YJ z0Pb987r0H%ZgA&0H-bB#Y2qvZO`I*X`f{o8M+0r@DVaEU`9)#wQ)+#nYm%uo!h;x- z{bFLXwJfLk~;M0WcCST>f~CT|3)3TG+5SWmJ9`FHe%-xTyU zLh0dLSW4QiFJik)J$rP@xCLal9MPoVjnTL{3}+ytHwK3ULtHc%$^1HPetWwBEAsT) z*}fX6U!H>w&RIB3)0)zbIhlk*MoD5gS3Q4-B;k|*_DW!uhRGJgwn!YeSi^FM^ED;w zNNnFIm<9w5?EZW>;vEZT;x~3f)l!eBMbaOn(>cCi}&y%9%pp?B^5Nh@%NP)ppv^b7Pmp6~H;|{M}b`S#Ozq z<02N0K&WxGL0WvJRQQsKuDp^Dba=3@mMT|PaOu{MAYmjiy>_K3?J=w$(IeEK2S#-- ziP`d+iN(K9HKj7@eH#ud7Vk&{&Xqc^&I5Y#@YRum$PO}`!0#vba!#8QkUaX~^*p-# z-4!C+7J{UXKKSlG7pr?cU~1CaG|SQJKzJ8nZoXog*l_JWB3zPB(DI;D>2RiL)Ie0N06y0oeVO*R6_1NhP zQw2)g5I4a~jG*dnqsz7>#N-ttdD$g(p4S9hnLF1WUr(*z>O6&%KEuKM*Xk; zF}~V!WBbi9?T=hJKO!C$we}N{E`1j7u*M6AMdjpn(53^Oxy%?VVewj%fW#y!D-B?+ z4eq$pjXzCNHlj7*J)>!0B{-3Pe0E#aecSBZ|Er&;(Kj9eOS9rW^)hPbuk%Tyk_4Du zZdx``xo_-p{qK_)lcTiZYQE25 zlX3UQx%kNH<>C+kf*V*OWFYuvP2FL_CoZQLqvWytKkQvS%t9vEs05MgFy>|yEgOgr zV0$08z^@SsE^N?_*{5)HLg0ks2VJ|-U_W~cfPbi_EfWs92I0z*CXPX7EiB2%qG`$1 zFLkS{?G|CXi>QIIg?{?=zuL2}Ak6;wp7;RzEw}CNk8vnNgzSNM2irlA$f@1ZQsC^9w#Tv++SwP2* zOK)ZronADVQGZ;T5^XB^AZKQs=^IaG7A_$SO+=msW0plgQY@VErg_XthM%zG16QKub6fTM2C%5T8sg2EIARY1-=}A z^zQ5RTnLISAmqQ^EVP(u{oiwa$QXl-I|c#he&MO(aLyCn&WhQeB7ZLz8DsAB!31jFZ@zQs_*Y@+A|o z3M1yT<0fL(dJ!-ayHLb>k+i{uI*oQVqi|R}4T6-^IPx`ehw_6s((IeUxljDZW|c`Q zkUWHwBB#+Dqq1!#Tkx6@#%KzTuwy1V1u~}$+>U42uoVV{)M41>nDoktcv8q^JHbeY zF)dH3&fsUwXMYn<3|tN_zj5{*iR=~<@*o48D8d1tya#U7C6OMjgA5W^Ar@eKA*SKy zf(&ekV~(e{Uv`0VGb!x)d_65YbXtQsq4C>?Fc}LX0fvl_yh3h47+T`NmvQ zs(g?_3b`Nz)QgI2XB$(=L4%3)>N#aRgUlmS%9RXKn0zmG0RGqU^Tbg>$;l+OyLUs- z_2lRcIVPtAYdG2b#FQ0;`vb0Dh65mDbHhmk?p}D-10N0Hx>8lx?j+o$qRsqX)s~C} zvySMLZ)B1cxpLb zzyX(2IhjGSnh*dwK_bl71{T7X#ow?Mcw8K|fUtCEU>LM`**I`P*|(L>d{UV56Koh1 zpi`&bs#osFCuPFBX64Cz@@z3ul<3qljf3fuLO4?fVV3f_O{89Vp@1x&%T@*sigo~6 zg-x2CY3BAU7p|h-3j4&~auR1z)tWyvE8Fd)JQ-UsP4`)Oz)t?Ms|y-^-B50?vCB9B zvJGmvp3uw`#Z~hOop1{bXJ0$8x*tpW7CJi2QG@NO1!tzPQ_hD#Z>7>%PO_EXW&)nl zi%Mn@*_g}Pk^q^tH)>SX*ju2`v-g2{8X`T0oW|eOjlTzPxvwERb&ct$)+tl?I z)s^8hXpEg5C_#oRIy(6U?0~9gj5W(>o1B|%oP)l(-~cHe9Re#WV>h0>7DPwZ5~nTQ zI8nKygdE_~C$ z!vc1_^43y5$Ch}&lOwVXh^xC<>9b>K8%b9Vm62@LC{Qln3lK?9o=T@E>QVLc!E0)3FOc1NoE88nc zjo=WK&sUOaz70Nmv66HP$=JtEIRl&YU6}ZO9X)zIOIcR~5c#@JIaEXPRb&e|510Xt zu~^qpl?7W_C@5!Z$dbGzIJl}t3)+QR9cWnn%C^ado@1cVqNLW6S&68m0YK`*Y#WKP zp_VjMvbJL@h* z)FkSambs)pH5o#tEnb6O`Se^MmrfDMWk)QfOll%88>%^_L|nj-UeX#>TgJYTUDc4& zu?~h))Fl7Bx@ZzLiq#ETT(H9XL$Ot_3@;^B%Nd>MfspI*>mssOyVroWMCAKm zqC4eA93ZtahHWJn=ILU+oeYl^3-^nPsTrsi+xngTL^FAo6yA!}m4h>rgv%8e0}WZz zN=lY-d~f0N!16E!uL#AK9hjO;1{(~w_IA1FQ5%W;{kMCT?~LICFGDeIO2OLn@0qz-=_hEHCHY1gCkNgV7{^sz0A4Si|gzzrcY z>SOEVZgh&!`D|pmn4TQF?V%nqeh5+FBtYlAa9}&&>}A(nRxg8F$X^l99$ZH5Cq+lFj$`QD0nYUKz?OJRup4Q~Gio^}DqSmx({v{!iRDws zO5jr>!>jQrOvm}-`IK!cHGOUkvBYVs{uNPqcqObpWDY=)05oKkU#=uy?!mR^O;kA& z4w_D;7Aw!f>1C@C0a$JTM_qdSrITsw4)k|AzS_Op!>rZ8iCN z{bM*Mu&Pz=c^)<=&dwnd3`EOL95}OmH?vP6_vn?*H6*>2t<5L(GghBAMz=v8Il_0y z3!u!IyxflH7$pmnqVlsfWQ(>gkyC8$Bqv&Cw2dXwrZ)jD3<5O+ z=@1^V-pIO*D?eUOs)b*Q$_MMol$G(GPvQ7;72va%H;^Flwc`Z8hgJRso%hi}aR{4v zud&|VQse8ZBeeG&Q8~1c{3%v-LAh-c*;Td=Mx#^S53Wq1=o`51Z(=(tm8aH_ z>~sybv^7pZ(CN1G*__yaVA)`6Ajqnn9RhhOsFun#ig2n~_nj#XmZ zbi^^Cv~2+%(Fm__Z6S;K_+H08|0g*8dd}EQZXv%}%0mA`i<1A2H307!86~ZvNbczs zeM|Z4R#GX%S(Kt4vJ0C2=^nDZ;O8)L8Us59Eu6N#*9f>FDl1CCextMkZft{0l3UJT z>qS#QIJ@Fj(rTEEhdyk|JLEfX;o6Yh+9Tr?cX(P&eRGTH5p;sEWEBq7kCgMhB*T0T zbAr&~m%SvrwG2O)bYuwE6^aqU(g0j#l;Jrn6jw9(h<3GThHAhkaGZayEZj-BzO(#o zWzSAhB4k+3-o2CDgHoRL|DJVr&Mxw_U_d}q*6tzOnZOfvYPf}lrMCwDgv}rn1D3Ti?MG^M0 zEaiL2P}ip+VOZZcJSMYgV_Q;WSozhWmtoK~d^=-=F{6racWS8c2!weHl%MV)1wxbM ztTaGcV${k_Z$fDg5IX+JOlFbcN>ECOzHlRFYqnE!>2HIH64+aZ1%N*}o10{}AI~WP z)c`y^WWT%M#h$yf;MtqV^9Eq;S?@3?-`q#4eDe2j{d^ysPKO!HD!}7k!=TTkYNzvM z$C5Bs2h(vS%s6Hcf$3k$Yaw*{C_aEa%#55nv9rWzhV2L#h9oqJ8I)??RDLIuT-)3HP1EdCb1sR6{FV#fC>oKwQRwQm z)#N^9eJWWDwl(6FKkX;4o5u94%l~?Sl&oiRI8?dD`g6-#p%{_ zJ$Smoe)~$$DzD84k6R~t%C*qLkyBrm55^RK?BOKi5%H)v2Cu*3-x#AKN!|w4)RLy& zp&DBPyJMhS3<9Z<17H{|63JfQh2U_TIX6WK@o(1bbwzp#z>VuvMrE zH#Ms!nnkpO17IButdvFcNl=}{&aP0|_kwwn=(9_;o$QjDcd2#|#V;keeRec-hEG1j z1DqfZcGlY0!Af+yz>*noBQh9JgO>^V4!{K*(7c8F0JSr8F}%w*z>~`ZGT1y*6?xsP z$iCZwkHpsvBL%XPE40uC7R>q81~6LaV-E>Jx$atTCgbziaDfhOWE_CYQeYCmbXvr6 z+O!b`Nw1Bm@Puc)d(LHG%s3i1cgDaG(J31XCJy#77XSnin2(kv( zAy@y5J${TmG*u@K`CdMy^DyUpbQYk~1Wwqe=ggMf?BXiJG@6PZfS4y9KG(yK!0O9SakC(~QN+F?%7>BWJZhU2R*C*_<1 zvxatNJ`G=80{dnAuIh9PdxZCOx@6z20=E`!%Q-dMU)T^#ZDc8n16Fv-=}e`6?xCP{ z4BunQD-7k$3gpcS<;@A?&GFCen9N&typuhtP#egu4P`e3vKz+Xal>XXYo1>e%5Mnd zH-z%%2J+|no0d=JuQHtJDQiCaFK5bH@LqzMS$(tW5>mKf8g9BA-+o#hs9GJA)=Wvs_pZEa<=v|_ zGv%A2hT0lk3~vTuX;R|I(6$?Mw7OTw(-2H_U3@jr1et*}ZIdODKYl z0RKgvXppyo>D3H++L8vkQDA^oKkYR=c&UM*@?2%TlT<1r@Td|Qqiko^E9qLtM|<&gL2DGj!GRuo-MI5ZG#^5uHw}x*{8RSRM7S~|_ zp!DpuYp;}W&9`5}@*Pcrnyx-FO~C`~C&1!{NL}ATaO1R9&;QbiC(`JDJf2ENX_igp zN2evBh0kT;Q@HejKj#PyTp8rcU$)u!OR@{6r=5B|m%kQU69bGNv*xhZW$R}oe)P-Lbo~#qv$YEPj?e5A39$b2&1{(^k$)vgPw(zHaz`lK2!@Xr3&oT*WETWCjEmUYkYta^tR#VU> zj0`Bu^T%5|-AbRyyPRDwzNpSF{62x9jaRwXo zMZ|mGgw=$2uM!`jiyCWO6tFFtv@Jf~{&rIO36DRoHkee$Fh)thS`xBW2dvd+e7E>%uE-u3$r+eUe$5-=EB`kXkjJPpq1_zlXjYE?q_Ufj=ttQ4- z*5dVJLX4?bumP9I4Lvi&p(TVT>*EDDvSm*#!AmQWGpd#r4W;Abaghg^@I96zufey0 z^LEA<-j3;T9yZ4Cc2JQr>i}qa4oyjk(0-V3{^Mqzi@`!PU^giiyEzj;wM|Q=^JFQ7 z6=3N0Piu)O5g0%fTe4SU?ufT31^BE2yn3%3FcN$>2EyoPx3M75B;8j6y2`nh2y;iT zN@-{hPuN$*wB0~Y&psvqqQsL8_5(!efEQ$>_sD=72TwJ8#J!a?nCabM$L)v3h$u7<=!zn zT$V&@t<`qiq#gjD%Hu7V!Q)Nm$ELqeGox%SC2~xA>C>lyBg;86!*qtnyXb%520ylT zZx!39vp#CWWGsxgY{rTTO8z{2(>c}{hBaMDJelY(*dDa*z)pbmUoiab&u#~nwtb_2 z<2HZh_Mo(5s=A&%%m?y^^jR@{EHJUi!2&D$Uc4zi@p#A8L|sBEJkA5J@8E+RA!%+v znmg|Cg_?E-nsx@IzR2S|D6$AijRC1~ye%}lComhI4uSF$GC%Hlo`#5h;uWtHz5-Ng zO;GBbs-ATm?2+U9JdC|bM-GL z6fB#ie|eS>{BY6%x;vS8h;ZZU1B{O4r*m%huz)WOu=m4^4+F_jj+T;&Vt7Kp#2yyt zm?nuZN9f#qM$wApY9~C@2PIhW(SiK{qO#zOhxf}Q2upa_h|mA1PgUD!-E}EfA#|N~9AI0ra*zWLF zAl6`jaYMWhv6hA>z}9zyJ^Fl4wcTq`Ph`Mr0!zQ3wZ)J$7Zo0c?s$`;MKVk2OybKG zL?7ev)1b1@JD;P>z5F2`A5fH!Y+Ov+prf2AjG|8_q(nxJ3aW(K8}; zr|<=67eBZ);TD)<8ZOX%0?;kI;yLZ6z{g=fZRD;avUv^4MgB0+(nHwFh6gbtaM^Nx z!TIV}hWy)h`ETv__YeAqWdESs?{km&Grd9S6M$jxO+b7XD=5{$mo@uT^XHB}QGTXm zJSkYT^n7=yee=ck%~QEWrx%>AJ~MK@$KTccV()kQ{XN_Kw`})sAM|GroiH;PFA7+T zxV`c-O_!{VSB<(3vW57ImL5O)!TX}hIQHO9^cMbLZ?}KrCV%&4f7X_ev^5}YW#&nH zfOpJqQjG||EaxRDQ55iqbF z9RoU>(AkO3W)_BJF$IBb__-aO9q9C;Gmh*vx{G#vIF%3FfVbQzs;x3qS`HN6*Y$fkGNt`k6TBgCcl3bGn zRN};PK@c-I)$_IM@gE{7-uZHmMI$NT(gxKH<4@WNHv;--$v?; z4cB%NtFedtwZvrHsM_kq8vpOizCQ-t^ES3b4)HY8@5l&$EUd65)WhsX?ar$v-VL!9 z=UJHPDVQnzsYc(65qdv@)%_oAzVik3I8Mqvx^>`w8NGPM$q(OGqc4Qc*C8SkfeTG1 zgUR>&iZiKa`o|k5^PBu)&g(g)r~ALSB%m4$W?A4aHky3DkNPLGXxKPAe}>t2xXaob z!EgL|;9Rr-k322={lCJlKB$f3ifbh#KnNj_03jXHA&>w80^(rTE*Ps|0!FRHM`G;Q zPHKd0NK(5eh=CDS+Rn6@DSv2&c7&%N;5Mz@si&pWw4om}O`4|BADzseMSF_lw9xc( zI&EpKjy=vleQ%EwC?<&*e%d|l+kJPpcf0%EzV~Lpbr2*#zOn7lfKB~(n-eo8abB-z z?%%g$KNQ-bw%<}v;*&aC6aAn|srXao@G^8k`Fq*k;Q1D2+L>|WX7^y;n(-2^IC#tU z3lH_6i%t2nu-|1Uc>{(>d6)>P-{T}+4WXB1p`)clb0edlftawGa2=abDKT0ff(SJ5 zm!+yHR+`yqnHM5+TKLyZmM|%RC#1t@u^3KD>1Gh48N6%9$f*HPQssg(`dnEuz|=}Q zh+n&vFU`w}V9S(RV8|v0NLh(8KZF^e9<1qrf*toL$LI1R`C+i&3l*>cRl4~R)Cq+Q zk$e~xHL{r<2n9;!^bvhlxv7Qcbe^2Kf=T;A{}i{d{4;|NI4qc`lA}5POEND!`J%-Z z(L*>}c+~NuoY$N|;T+8I^7V6t5rc3jQYai`m-s6$nz;yQoo*B7~BxJr}*}fgyrwYv0$Ny7CxZD_-AI?!_-9`s0;=qeP zJbn1tY3>c8lZR38QDjvZ6l&Npf1#N$J9Dz1)oa`~BhS!O>_T&Z8M(_e+)kR1EmL|N zYLG42JL5k%P3+pEnFBH*N_6A`erD8O`BNj2^IeRY?25Te#WtYpeqRPEGv%Ivay@Z! zfmCRVB1IFOSg9>9Oy(_+Q&=dPP#QOAiYDZS<4YI7WTl5S(YFgLj!Y9zK@&$5=4dpwF5f$vHjbI;`%Z<$$x5KMVG~t!9mqxKCl|_uu z<9GC=oqbdXeY8-hRl3!A>{gS_O1%-I(zAE(Xs}$;&KWNoW19K0_})_554!ZL!`tTQxZKAyZR%4fPhBq3qbm z25-tw2xs7Fn04TCO19io#B-Su7Nr{U25IK%`5Q?88H8^lpmcgz_#xnI6~6DA6n!ly zM-}OdT;&12R*;~{9^q{y{0SqG=fLu+G0LLVWfK7nzR+h|y97|zjzUB_HZ%RuX*eiQ ze+_g(0)ZKX;Qj=8qdV?r7I4dHy+`;xvV!|Vgg-F7Bpc{Hk9i#a1U8ue0#jc=VDdAz z0A#YVZ{e|Zgbjp0BD{mZ^q_yj)Q=GUjF4F#$`evHH|R(1*j@=_HY(i3g4-;!9kFP> zBJa9p+vyqei@>~m1}1s;KRbiJAay%J2SOJ@H^KtKmk_>(@O^~8BK#QPC4?^nLo0Oo+B?B`bhr12Lo)tGSgWtIrfN-J(6QsbPUtS zPf^EkH0e0~{Lu}w3l@_oDw9ktqNznPwTY&-l~1k~ulmjS@qG_ zkqx^$Wv#pJ7p+ayT9>l&>rU_DGhpm{;(CB``z5YZfXD?BBlL+;$_3WAP>O3= z@rzs=j2YKA2Xis)p1lu5pDxTa;^rYh6*fq`#v7x?%I<1TOg|NX80_<+vn` zqM=bTw2FpS`cMz;9ib=At{Kjyjck9CWOj>Y_e)0vh~O8!w|R_gRrEE8a8l0ySCw1r zy=}2!02Nx@{ql*I12i~9haaUwkI|}QYnBmcnfESTzH*uR4kexbSnr$V&XkpttW9u# zBx|o|?M+ze<4>+#4AaS}WDP_S3!j}5qS!&+hEP_VfOZBfQf3-Dg2=dDF0$2^06-4@clR`K;eQs8_w>@B#j^&|ea) zP`~OVka?BoFx}459rcj@Rqe;BgE{Kg^(M%?9<_O)6wN6KmT7631`g4R{J~r;b@^?B zYOUC=gP(7zwQ%TWt_IWk{Gl4nO|xG&~4Z{7OSQuQV6puat}vZL_>;L4x{V{LCeCGX9%O#5Q6G&J0ZM^$kpRT@c2Ea%>63 zH3DwCVzIB}fG`|pv5{K^3c(|=X9hPd2~Gqa;V!-b*D-Y`UN}vx(JD;45VZK>%Rw+9 z7y!V@>kKM=gla5MgWyK+06;WsFe)1bbC>W~Sg6IrolyBLO|UN@)M56En6-=o*FKO+ zj-H2#o*4VoIkZ@<#|qq2>gz=8Sm${!)BVP zr^gNoKZkn(wW-4MfDo3Nkh^LPX*^Btsx)LbydxjaQ;`!yQXLmn$Kzj}A>F>cZ>t(M z^3AdSWIq3#E}he(BW2gi(<=N<4;t!7KUv;DJu1JZnN^C(zN*oTZkW@{qREx;p61aUC%xD?Dx(~ zo9*v>X-PholoYSvXUE*Fjn7WmoSXv67q|-i16>0_iKCg# zK8MRuZ*>*Qa*?YD|Jr?n{l%_ge~GKa-(Kn}l~cy~hWLlLhRAZf&*?97l|_~s>KclJ z5`5+UVXk43x(ZhX)+PFe`$xD&$hsuoW&V+_k+PiZ8|5GE8totB8Y91__{RD#cU=yO zs%V$jr|t)$gQB>uP?h%SN`2aGN_{^qeZMJEeWg~WDp;S6^%?tBqjtaXM{!j~S~IaV z>j$mlBCXljn)8F!@p9f=Y|GO!<$CRe#{^m3AFK0wpiw1j2ViZ1W{Dwpm8>0zwT>RO z6J>27)(+}XTPK6+C^^&paqp)o+uyOb&NIc+R6Zn@)24_-zkc6W{Io*HYI`doh99Gp?vGjs9^BHBF9cxeY#8D@>vOx^0CP@`OT+4d)BAidsEx>e@Q3IphsA>84LMPY0s)oHk#rRd{B@bJokTz6fuv;I^gw`65nu*DZ*l~gmDz!(B!`Q{ZR17}ik76NCA#22^Ky*#^`zrCVVovZMF zuP7~DOkHOas3mX>0zI{v1yfM!A^kX_d#Xr#ER@ebC*~T<-~K?&Ym+&k9Yl10xJ|OEdH0wXAg{ zO(L4xWMLNM5Ol+E?gEhg+{1%DV@IyO)@7+WW3bn28AcIj8LiZ#d@*pS8!9|PU`U>frk(TNo7VSeHhDa<7rY} zaZ9fHjlyeg8E@S~edqDJZmCf>Dg5nQ&bIg5DDfBykrraK1j>OX+<5)?&N5PvKwQwE(8@nd=9@Jl?o|n!SrSj0F__oddM6E= z!*ASJZjY5-^}0QxLRkNq_R9I zcARGN5g@I>9z&p8e1SEnsN@c5*EoQ*F?)i*(*&MZ_`q$K4Sb!-@+ji46g`FrqdrTL zJ3_qyIv?EjfGI746Asu!qc$si&W<8Fot7Q9UnXV8KrY@uZYy=VhkzikjljJGB(B4G z`Te=mC2sZ}xDg={Hw4=W>>$w1E_VDH#nE7u=dh-2ESVvsy}AQBkKUiJE+UdlBhBC7 zK|R|=Mr^@eYY%csN}a^&ZW8V7sm1gMJE3LOzvw=t~&wNGO=dpjc(y_$wf z68@7q#igdw(sD$L6R0UujR9b5z!68H%LaurxLfd&HC5vUr?sVRV-2P!^ve3*)k3>!%8aZ~~Va*O#`w^w0u zMJdTvY9jD8{tuR6Gm88(`pzg=CW@Z0v6T-|5+WE!pzh$oOjAeDez zGL1?J1iBYYqOS!04g9P9ql|TujCE!_+E(!zqM#?&wJ?V6K*XFfvK`X*&=W$qO`p3i z(1P|HaXSBK|LgnUltC*<3P_wVV2LQ3(KL%hHjC=A2}nHqpqNe#1gt7=I55FLF&%j7 zM>(B;I8bSZz5M&f%gf4XI${?+C8)90-a0Mfw+8j(dUOtnebKQ&`^snhCVz~iG@XfV zLf4TT24hSRkF6yrNzy$I{RK}b$kO?}zZnhb{Po}DC2gUhB%e)MU5H=!O}33p9O{0a z)LC9kf~i}ZJw!Ew;mGJnw-NtJG}{XJ`9oRpQ6D^8?Yy}&2PF5^nfciVGWfwK3a?}Z zxN#@2LHK8{*${ZLDTbew5)8EXf||B6x>d7Wh3q?nF5l{YeKC9yXLlesOLakF}MMUMnv`R#m#VWdW8GrlGWVKS|{h!P=@KnXbX2=lB zsv*;MBl1Pz>c^rKGlhrvGIK|xgf+2)Xf60}+nP>5DpFD`R`q8i> zb06Z~W+`2ou(LqXU34OKq=zrzY`9o>py%ysnro{Ri6A}s* z1P=P(8FV~7U7exwSDyZbK`-E*(;1!qXB=h{;EhLyWs`h4vkdA>ZEcuc{^pUYIkRYX zDIh>4jJp*kB^;yf9>T3>McLb6(}>$$&j?dPvD3Mt`|o*fK!0g6+ycgA7HIiHhh6@7Nq4d@K^Xh z4(0H`(R9_XcHVZhze>*4gD)265Tod3n#tIuH0VfK{=ti*6Qnx47JIanlAQ2SFQuug z)y`=ztu>FCM>B^c<@CfE)$`r+rp~`+;k?OH4fm7$=|BqxFnSysOx6;l=%ayuact$R zzu=$sF+d zh6>bBW60Zt+MU?aRwj?<#5)=4DV6{Aofh1{;?pw?i2^o*W}A+nr(Dq3Ei`tx9Q)Df zT=fH$e}B4Qu{2Cl7}d6GFgH?YrqTLRhURkr zy93+j(f6Mam_&f=1shAHN&-~`CKHf&V|t17@O<89`5v0uyiw zBalp5?i*a=o7Pwr22K-G8I=hq_5kp;9}eQkb5nQEe`^K{o1Fq0zT&7WoQ z+dkepTArMV9ij6goEP9>u|MFqsEUgnQMZ2MHJ_BJV@$l|lc}gbt0qD&KlKT(ms450 zPg2haG)c4TuEQfEH>_?YX7C=PlP2NCd}*L1sLer>6r8#my}`YPd!<`6`gRH5@Hp^e zpXRHTCf@bwe=fdX)7^EPKAN1V}?ND*-~4k~4hC-J@9KTX4;;o@=@R`FOJRBiF>b>To~n_7l9wVm}ewokP%N_EKsMK zIxk$jUQLx2O=>>FMeJPs{RXvtC9SMtzU(wj{4RlSY0YT}E}AMTDws}nblMl=N&jKC zl4{?>+65S*zfY}lb01LYWda`(knZDIETNa9g{C%nmix5&=+VT`NR?_h7&#l1pslN5 zl)Axm=wYzH(wGru_u)i8!foA*CixC%@h$?F$T9WQrye#HkOMM5_r34Gs!FVS+aj6G;a$VkGt!h>fX` zmq;Acl}y!P5QkFXI!wWc(<#?y|=~BA{;74=fWKI z9EjUUeKcLm*O4M$;Z*0Uv%PEvqLPOGUnl z&vRjm;q%f7MUEn&0y#~Zz1^npV|nl=bAsd`8Te<0?;i-0P3`BgKl_@%zX)6;fX)#m z7r&*lOK4?sXAU?N~4pkON|;-vR%%uU_XsHq=;bOe3Pf}UceqJ_H%MyT`5 z;mt+hfJDMi$D5Q;3v1$#kR{$64Ex7O3H%Hv?IO!;WI6tcUs1}x$3J>PBn=|X$328R z>T=Op41;CVr00s{637)_6~krfa_G`l8R>$rH-g&OZGN+Kcc(EJryt*`iW;Mz|r0qW zIGlx<-E3g%%Lek?yLsV~r(H}3M4;I$Ub+lsUnS?GiaMlU>=Pd;eL-A%hawbBaY4P} zQtRigQyQQiZi{o7>ebtn@WPS60p^LWQ7}G!ow62OW9BQ)`C|5H)Kd8-p7PjWe(Zca zf7hB2J}?>%sOk+P#N7Wzy?aYC>c<0j+Q);^iULr(z=#;~dT8%lTwPD1ODs2DD6aOR zs$nt?E2oBBq$IC;I=f_U^xKc5FSb*AzUbJF8K$#`z;0s{yy-b2KMXlG8N}fw>g$Ig zTV<2Pq)J$#9t81VCFGgYkr3i1HeHAQxT2Ifq~jAO5EV6F60oR0UOoD2+Cj zPD%!pqY>F278WsLW5v!YD6t-;Rr18KD#)|GWnYYWFVTTpg7GxxDBk>hqF8ekOdA-P ztr90TwW{HNZ5nv=-9D zn5$usnB{~xu1|~?=gm-LG1pr{iQ<|SkS}s4L*|yx`+LkY)*tPQ9-W`=h>sREy*bL~{f#Vo0gP zlGbRxx&Wr_3`du=W8b9AJnY=oe~L#mFM!;tY;-(=M{)HIY~Am0eL(tD8n zw>0kmrnYp?PEfzI2yn?r3`p9J$$LV4J7gFlX>Fw4iCnW$IEr=@ojE!prdn0;vKJ$;N&cfR<-h2BiLC0u+xWJGv8Jby9dscI*Pz*5Lc?MpVXbt#PM zr>A`vFOL3+yZgiGALDREa3yoFw%*{4aYsOM{Sqf z*KYw{LITC&uDNuvabCXTmiVj@_Mtm)pBJjztLZ3`|FhGT4l`+UJwcC^T*+uBH9vi32y_W1w3t;UhPI8H{j)M5az9FE{+r zN|>!C-A-(A2ZCDmHxT2Sz!lw^l*iq8FowCNK1KqZ1h_{i@;-f)8dXNw_%aZ=vCh-% z#XC>Bqc@a{5s}dH2J7{*($uA`IHKxBW&jG*M=aqn0pva-M|Gh#ODt%HleU|Q8@>fm zz6!1=Bu%9!Mz3ATtfn8eB7qhJ3;}63@j@h&OGBzKu$D>wnT9bqn$HC zP*3p&-3={xlOBCIFjxfEK&EX8bt?yP*BYqFr%MT$7>lue>}lIVaefU{C(7tX>c$EX z6V^guQY?c^$qKP@Eey<@gVXe3_w}mSxfUh(WlQ+wwQ$x{x($))I*h^bG?^^v-szX5 zv_IPT)Mtk`vMs83<3<>KIdOI^<}-E^vNX~-^=kS`*B*KtDki_TiS&~^4vFS?QFap) z_M3>kg4jukXyFa2SaK7TsGnNI&u@aqi)2h0J2H8r);l(>lkt$a<`9e!_umXvV`EE1 zZS@{PsrR717W!UQq_yL!-&(}7z0iNa_~&w8o_b>blDF(jja&WMw3%80*C=9DJAAo( zAAY%LE_DoZFd_ikjH_Hss+LbB9R>%~(z6Jxj%HwcPJ$!$5nl9Awr_aNwqs>mnYeHp zkkr@uZAXwe&nO?8zP>Fd0EK$|V%o<5c?_0-Xe& zK-c|Nj3%T*xkJ*nAUVpcR%D@Iqxkm$^cUN1g_-H{h?C?aPQ2v{(!C;oGf}xMIfm_2difba)qE*bBRA&kFpldb>-Vc-*FMP1h{=*%Kb3H+Cfr7enx8{I z3`iFL9DY$Qg)}kp@Uj^nrD+(HkTzk@^qqL8NO=MS2+eeolqVzYuoRH1!@v)wU+%7A;XLQ z2lu9nl=bKqT>+wcJ#J;3ScXoaakA*fUl2!%%# z(SHMsTp=-&l37gE_&ggmAC@prEIki{wDpGF&WI2?b~cnsGoCO>@{m%)8;igSynr@d z3`K1E9P#)DNY6Nmvr7%P%;A`5xG0mw=?ze!mRZC0yTPZbFR5bnCKULaRq?7p%~_&tsldRnjEh_ZI63;S18k+m7un`I4GZHB{u zcI4|V;H=q=(@APj-po%oGIU4WcfH8vhLs*b;+CZb?`034S49Iy1%911yph8Vkk~=E zeoIr`r3%Yd7?FNA*7TBqTU0S)D_Xl{ROU^;*hoe6dZ( z;aDgiA2Uw#OBKNeiWlyK@%ZvWn6|^nq4!{mGv~+coy1IL;GAOF_8vCw#_f=sx0KEy z4RaY0wHM_Jmm(g+_Ii2GN$vg!LT)Ly@>N@paX~k|OQJX5glCLtOJ$Lu)TX&^!5XRw||b;>b?0tDCIim7Q>XL^!K&F2D)|UZM-a8FS7TXE>3%FMA91tCW3?*+fqM4}t%>Dn zLF3F34aFfphJv_G5h)L2&S|$bT=_8k5(-XZ2lgI;GYER06IaA1kD%y0VHH_>px*jE z_UPk>@+f3YCUk1|n)>iulqjzf+MR^< z69mEt`V!@X2vOP}g*59M#Eo+d7vk`vFw*e($!9PhfK8M7plJ9EF%34g+h<^((sEDJ za`<0~W&%Dmd;@`N#o45OdCMxkJpkh}rPCF=?~}&BIQOvo*KiAr`ZH1eT>?@&(8@YE z+AvBqev06rXIV(3mDJ@g1g441gD~)VNgpJKjG^R(UlpMyBZND#6!o@bJk%Qj{56Y_ z(<#AzKYa6Xx!8CR%2nMees>T~8#_p*l~V|$BB(Qq^QRyq{OsfK474}Vx_$!91Ofyo zgCYILHp>Xdq~Sr+LgsJIHd(6LXXSze(*l$Vbldv7!_ zBhjpB;+sx9VCgtsL7r5(+GQ0Jo`8QCSH2lE2gwlLyx=tmx&1YTblp+Q{+rgwMeqZ6 z`bzQCAsBA`p18(AT$5SuI$ZE_?uiA9PF%0OWp9u^K0d>VyvJ?EYdZN0iUqWgNm55l zeiFoRs$fKkTJ{CPs5<|OmZrAeYNArag<+@)HZe8~RYpABLS5I0_Aq=mQI}U~23vbiKTbSz7|K+uP5kXJEJyEa=2Ng_U;?5Mbx!|9WAJ~BvyTDAIho?U zryy-mGNub)W2kx@uK_)3|7-^;dw{kZp=c3@k-$Z zRXv~z&pJpK&pZ!T%RDAVWfUVLTmB0F=r+u5p<`7qHyxpHhhY>RR>Z~^VXS(EO$3g@ z^)_jl*hF#iC}icV!}Z%z=F4A1b%#xx`>w`p=&9jJ%C+KxE4|J5s)_mkkJYBz#EUQB zm0Y~|_yxGVK;A3TB+QQB(g@D7iI-lKLq2&C{PXGYiEi>VwaT+G;#P82rNYu}Ydow8 z-IJ&o_8=@BgM7I{*;j{SSyUr_uZle{L6JJj7Jls|n1sZYZ+GNxVzz?d@ch8Rpb#q? zpF7nXl@0jl=|YNG~33!WiQ058H%~oITkVG7z~Ha z;v9oAan(;T_IcnKoDt>6pfJG{{hcZsOzqiXVh~F2`4=RJVaLFcVD8h*PhZFosUb+G zF4>Wt;V$v#zp$#o01^>2P*U65n=HhWPAhynuPR43q1- zUcba-*4Siu{PEATV=LzLT+tTm-HCa%`U|H%|z9*04;rNm_M zswj93IxMFWNB`3rehkz*pxw}tB$Rj`LT1}Jw(s5ZFo`?OFE1J8#b6C>j_$PCJSxp6 zuz@n_ne`x?-Tfdz=s4bCvXLUD+yHMug3Adhlntqbv$!wS0?Q=$d?KipCGfdjwak>Tl8Qq=w^swg zX==J9>+`bymh8_f9SEj`PkagqYKF-&=&Se=%W&f~XlCaW@v>Q6+^#3iX`LGyTQN5@ zIlwg5kBBO>Eg4jj?^v_2q$fnXO>glUFVpCzm&tcX%V-a~30y&4`(f5R;u};nwBYL& z3?lNVbAJL&)Xz_#6~R>2f)GtzYcu^6R$f02pl$^OaWfc{9 zYNLV%bP+`H$m+OWpyI9vDu@V<7kINC3wR;w`mb-jIg%Ln_xu0<`Ftq)_3N&#uCA`G zs_uUC=94i8Uyg}+H9ESJh5m+Id+YL118$B<5hEVyktzCF?9tVOYl`O;3ya-ikEtF~ zGj!fiVUZTcP=+$8dZ6UTNmc8zn6lS?rxR zSnLUokiD8OceFimgJs?rdlJDCdl!OZ9eG?bnM$VkYwz`;oi~o(rP8}JM^*sKcxh=Q z#zLIZ}MB zHCpCP=IkDn-Ln;nDV&>6xxHGUn98}mDYs9nveP)XFXa}rDqF_6{V2E45f|v(#hlxp zatAm<_iA<@L#K20K*}ycwl8-E=MJLW!446~oyobylslvq;#r(KlyZl)%ALI?B!76z z811M?OkC!0m9MIDF0Za~y2_mnePvap5gXb+F<*E);azpzvSkk48{%A3TiG%u^bwI! zM0B{jOsKA|s&`g7r&cU+R2P(&SJhUz%FDA_V#1t43^J|`A3t#*71Re4@OE-KT(j%y zs%KZ#=ZEUW$RC1yxwOU`>8P%v}B0MMpS zAmAg)8|vVyrO3Yspd)%F(F=F#1mjF(is)}dMBN!a0w|8P$QFW)0fl$ds7%MyRQ9Hs!V#|W)%Vatj;f<@VtFV`2 zAj+Mt3Ri()|RrgR-AtSq8M#x3bN;yS~V-anUR^H$Wl4S>f#gi=9qEHWZ9^2J@opp0=4 z#|n{ZELxaqoEaQtcrua`wo+jlW#DmQl!?v8sf>P!_fh)W2m(!(lWCb7LvD!GN2n{- z$<&sSnX^Ky>*Op;x#Z`Z^;zH?2n_UNr$=7?|}u2wrPNeZ8aBuHS&{n~bvDwD8T8%W8!UVaG%t!z(--cFi$I<*~DZ}haf z$`#YA9jj;S6_qRW7OELZymp3DVuR&d8J00#m_qQZQBklmzZd1}kX&zQb=@k5uJ1y| z?Z}9%taen?mbTA6*x8IuK9?J1X zUeVyt*Z5F8je^XuXYsC64N+R|Vlu1oirLtiQpO`MCPXq5{z^~oLSuU0Lh(V%yu#V? zqIN7OLq+DpcBJ+Jppt$uz;psEX+C|j1(^VM8s87-Zi@UC!#41atd7v8q4f0ty7A7y z9;t0PtslV~fE**WsJDsi2u=KqmSsf~MWi`>P-eaHx4PE_AUz(2?E4DPtoxZ;>% zYh)3Lgnk!ZjA)rrd`$>xvtz@@WbyQ3YIx4=M}dO?EEEfkq7m~;K%FO6cVhPYrjAeh>idlJk?CSc z%g~X%x?ZQZ*Q8$pJHT?|oiW)a_AgCMnp*O_C+1MJTTjvV6R(M%L`bQ z%TePj$BL96V)Bef<18BJNZ(SvJJ)z^TGt4_mEklNjq6Vf(2U~LmObOXwx&IXx}O0& zMZg=rl-7dU3Q~lljfW>cEWR=-ri>7w#$8i#Q>`Qw-uR08s`8pDr?aYd8R^jmG8dW+ z@05!gLm@L^1oFG;7(RU#z)65p03QKxf1=B4DpokkbqCFUr%V3?FRlP!o?6M6fp$fw zyc*e2`fQ}amsB`S!VaJ)E1Bz&V%bv`T0ix#@Vo<{t>)3cMjne=7n5y40>7YF-WXCd zwa%pu-DIx5$M||`zIfG0o;KV1BkG15S4`^@6I4T8DAU-nyGxik@AerlP0JPTmanFL ztc{$43R3}yCR)=0SOx7vszdqvH{Jl$7*R9RO$qro8wnT9d^MWoL-Wapku|GJPBF@7 zf#E#rrd85f2V@v0=O!7|v%-bfXqaV6bfr+))3n5veX~YueVPfw1<_AmO8=iuB#T>uXk`OH4|I`7IUgyEpxzy+T!8<9sTn8( z@Rsq|Wg|_dW^7rKYQ)dG`HvMkw{fHK+`NQ-XvrIki3eFHyHN*E+Nt@zXa(S$q0R3X z#`aTRVTh8HmKpQ&MCV(GW?H|4>ORZ(%L3bIuEtY*D+>55dmhq(z{nby2iMQI*2CM? zcw}KhU{3#Rge)8rRP#f+bgbfy2Nq^VYLbwfn%(HXJhkPUg%f2ONHCRkKeW9Ak4#0D zF@15`-~wdwcr)*L9%(#}yjEVHo#qg?HjYU>H!tog!tQx`vB)|NgzXpG{=f=w-gvjd zr*r#nS#tCAcGAG}Cnyr(R0CS7mK-mi+BK(lyKxx?^Z+*F)RMlW@rLV}EqyCj`9`Eu zs}bpKJZ|q^!4(1{qH{Z8K10zzKD~i-G{6`}PyK4|azQBF3A4!4m|Lf>aOeU5F5ej8 zSmM*XPdE(Gh~?88)=;~mmef0m2u8shw!Few;d1F_E9210cmU`-Zy0Gqe#vxnxQ55K zYPqtamRzSnG~!MdRrguXn_}OK?7X??xbe!erD8-&|K-V{{aC|4HK`)YIA8N&BXmiAvWbP6#FVqDJxQ;E%YZkUJ+Xj- z;O>;7NlXB56rAV4ODs^{NM~(Dy>oe;tGvo?>V9y~6iObm;lk4BlU!a;f*RjvNtFTp zWB_P<{Xt-j^Xa)@V{B}|tXgbZ8$M-48Sa=?oF!|0EZF)Y;X6-!?UJ@J@=VLIx*XXJ ztkgRc_>pKFAkXNk`viVu%NTuymJ!qrV1+lbzCtHIyu7}G;s;sAOAQ<2hM+RDh!^Zx z#^RL)rpLw18;Vw0U4&4IX!mOi$6o`O6Q`LShn9A>dRNL zpgMU*cssG_#&Li><|NlA2avZ}XqFhyuI?soGd^D3hvX=3jkNY)BOBv!`Y-&V`yWJm4~gssA%jfx*In&W{OS5zQ)wl8>qH7zK;9~SpMbgq`GOM-(;L@ z>?$@Jzci+#Ujp*U#&lHZ-pExIRj$%HeSBT5BcBL13Y*eQ52?bK-jpx4wYZxGSgp*Z z{l=qzzLlJ)iPx5yMHU#_uFVne82hjNiX<8O@WqWGR_jU-y{hGn8x4`=MjA}juS9As zfEABEj8|>GOsqHF-P|W-9=-BLRM_oPE7sICxcpY_HAdFW1Ntev?6k7VS+%69y2`bN zEv1gk+I3XWon!2{HYui3chD+azT8n^cj(`@+;elG^ilb>F)=*pIx2ZOoJjc|OT&fEV#cDYeIJM!(f4zGo^$MmF3tM!1X@?|I%YpMN@?)pFq8PeXI(2hd;)ND+ZN z{ZdMsipWNa=%xEh#nndaUJ7p+acfdrcJ4hd#BAfv2UC-He0Z{;ijJx3O;xq;i_~Z) zlj3sA>sIN{8~=WAY##SK+OKoU+0JHdM~$G>g;d+!1EXS8@9Qk~8IAi6Qj@Tv7e4fG zRODaK1S;ruBjxEl(cCinX|jkn8T+4`WgUh5NF(L>!n~(YZXeL%@d0kbl ze$WVcVThTneOP*Vgf8{PEb*i<^u_Mt8l(J0hbbwI>c%LMW_OCK?Ir-Qm^cY4EWs%jftWbXP@rf;ZE zk=jU0T3V;qRJh9R4HOWns;j-1$~gP0XV1wl#-bBh{tl@VJ2f*x6Qa*HO5V%q5|p~Y z@=U7OSo7Yc;yL5Z_j>n(ftVj>u3^C;jt6hUR#oV=WNPZO(Z!bl9y9v9zaR;Gpr})2 zrK8@Zzf2FT=(fL~Z}vLQIQ0GqvDb+B;7i{$i1~1|wDM{(+n9A~;3()`Z`5?3fj)`6 zNK<`bdia^M7iB^q`TA?rL@TZs2Tv^)|1`RP^t2EC&mSd9kzvGrnr6);p(j_d@Y9?| zo@Q*ZaKF$cJqNE@h)rtHkt8!z?r;{EnuY`C4!syf*#N!`HQ_0;5kwJiSl%3G^(z6C z4JZK=jF%SU$ft$km?1wSO~Sh1gU_xNRydLZX_8vGmK)8e>y!FD(C5cr4T7$ABLQa2%0ixk4UdL2@1&v3|r1)6rX$P!@z)w8Hjp$^-`Kc!fZbyP#SRx=9d zXs6BM&Nr_3wrgxFQ-|X5BU}2P&xHD#{*%gt-M`1T^g#jw-3N+lbyFHC+N=u+wmBG< zB+6{Ed=eggI4-AoM03I6!l{R2rX33}qcGmg3)c$qh~fRYU>pbU8i9bLD7=Im#N*Wj z{Iq+Kf$?X(-;VYg*6?awf(SQieu)s58LNJ|(=-aCDk-6@mp;G#RZ9xCu@i_`0w&)2 znV1;K!?_2UGu2QbN=!votD1!9Cn`J-3-OJ}gs}Mgj@^$oeXmnbYod=>qfTq0CyC*a zn?$OocZfJ4!sbxDGJTZlWEH)Zac8-AJZjuorshM8i%Ald_Xk|(Q(E7ESAo8h=G9#k z*U%TzJ7+NF>QfH$DAzpz1n+5k*+?JdcHm3tFQW2%FK}6G%}FG86zH z7x@{c+GBy@lJBEXBHn+DR2@<&0H5ITQ-IF^J_lewIym*XgDlqtc*}CVxC%*j-d7+M z6k?tfAVQ~|SW>@eq?H7Fqx{hiUKjpIl*=Iwy~T~5C!<8Y2*DC(TeFS3BGvm}(X{sqfM20); z4`3erS>2f+2CDgSVy;g?c@D*iv%+eQgLi6PR$5tfGT>Um+b_09>!1LuG zs+`2*DF7H${|iC&yF$OBILt`>KS(jM-;kms6&C$TybzQkx2UtpEAY$5utm^yeZ?!D zXVS!{q7gEt!`I%UnQUR+0OaSk-v&TPMqh$;53!lKh*2veu)yo&Gw!fOXF8&DE@IS}$xGg z18)FUQZ*+O(fJLQ?`1}4{&%B7LUYbV4Oh(>;x03!IYW)l6bF+Apaq`%&8Xc%pwenw zaXL};&l0~h1|tk@%dlv8BWX|6>8LihMCsg#>1OG#0Mn}ghXCdg@Wzy5?~{Z7u+o{E ze&yK#YTb-X^rt?wQk_laB;Z(CQfsp`5G^~a-nWTsX!cfQi$gsF>y$UnMTaV?Y1f^p zCCj{xS*7OWh+)F2Hs_E!GVL?Zu^h2b>r#(Su`stb!_>q)F-AnHzvPJ#ruE_ZFi$Lw z6gi$T1H^YC9X|Gj*vh)bEz%a=D{&#ge+!dMv0YK}i=NgNPViXDm$+*6HLBN8F)obluRN*FJ}VQ<&>LASI0P|I-7-}4UC663yghFW zyg!7V$W@>ihJF|D-VJ~mOEH(_Rn_)Z1L>Qbm{RVx14-^Dg_ats@;sTt#2HPD@Q|UH zFK69|j&BAC462TW-Hbj(@+p$is%qMW_o-8_)mhh|S2|jSp};p=AjWlT&Ibp>g0HFL(Ub1zLXB-gb_^~J`IR~PY$!yO%p^>$svmCIAIZ-Z?fFG zY`6Vz(f9+7qj^)C%wrMx@j5W^R+Tq=7Wo0w5ualLX9fG13Mmx>tt@Jr)W}k@U@(DO zb+jvw5Vp>$9kVE!i7u-3rJ~rhf1K*6QjsGb^?Xn&CYTOvs%PXxlI5v)0ukE)+mPDc z7H^P&u{CuKPRDfY;+SW7DrsnZOQx^452?RR64~N)_4Xt&!^f|Z$>Lsl=~5z3-$AHn zmzL#6n|pG9MN6&vh$6d?$K4E$;U?tm2G|3Dj{BGHy=vq%8ld57{xq6q$JEwo;zx6~ z8DASmjm;aPj6=6vq8^?uCaQgNWKU1obn#xO^&SY-ezoImS*R9YA~rYPOBttWjA#p= zQoJ~xj$nH5cpt!SfIR^B1MCIJ26%t~9YEl4i#b)1%M%09vv$EfZ!+wia#x+JqS_2Z zI?Jn7-CPm+yC9m{Fjq`Z=DCXL=!>GU{?)guFXxIvb8NS$>`TdB{lZgrsd%Dw0L^2Y zFRqb|kArJZfT+I%JPANqv@MI&(*WpAS$<4MYS~=%83za6Jd6#118;vv>Qdl%8GyU} z45gglCe0+9E!;PgpI5yWiEgC-E?y+kNCY`%tga~+pAO7ndlUuVx_{T z(E_Hr+P+wf5<}H{i-qsZ&lBAl5!1jp7+YXUnsj)RRQ3{)q&$m7ET_a6RlJ0J1D{fF zT0-;0$}82&YImjR)pZAW#nZ4ID*Yz4d6`Ixt*B(R=!-3ts(8B~&(ZAPm3FZ-IoQAb z#I9fu&fCRUYZ*z7GeuD#Bqj7%_)t24w9p|Iq=Eu}P@-3xT5meUkfd3t)rlyhJ%Y^K zjUmUvyHZUYeDVxgD!Pk-sPB!aa$bVKFdd^|Uc87*t6ov%q)qbny}22^$rW?e-Z-){ zL9pmZZ3L|q5aQP(!A*`f`w&H{O_qcZ+dRMLcA=7u!X?Oa&Qf2jnmQEjKy7_0yKNnZB9{~3HCVQoKxJWxD59dh^$ zbPZr3L@$Q$u<0m6Q+^1)NQCB_XS?3;3|T4G3M-U5S&0N9Ta@AE?M^Ji0mJbjMY{X~P3)SrVwD*- z`WKNGoX$V26$6sqYBe!4dSPOw=_5QN*NHo2F7sXJ92?cVvY^l_>N(C9Tt?N;jbfsB zUyW}PgLofT$Nr99M!*|MM^?>fB}K4rpr$Xdt>CZ^vt5YlzQoqu-pAH`;;`=1^d|5* z)*DibT*3(9l${>VJFas15wyk zgmi=>Uj2AIsr?ZuW0U9?S5NeMwK5uQNT@zUE!ZUHHv*f^{4+x$c!hX(N3n6M z!_j&Pz}G+kVtlg^3*Sm?Iqtv<_B6P?op?k<6Gqk!39jBNpR&Cnlgdgj=IE3e z7-D&21A=vbG<8njr54{Ha*J5VOxA(Q%RmBqExd~W<+*qjv=)*%%&*-OUY&Zz}@2MGK6*)f_Q()%|1=JamhQNM$BTqT+YsedHFhH^!;BTLa^5 zZqP_?ihBBKk&?O4@BccB--fsX|_7-l^V zH2F4U+YKQ`kH?X1ME?+~tgT|I=V);Lby<#5anvvHt`q2dvKfBtchkdr&-kY;_jdg)06rHDNGP= zOdtwCcPL0l4pv_`i=jD?NdG&!9l?hb11bourK^cMNM)_?T)sn$hLOvm+81_;C*@*p zC(wl1A_(wy!KsuAx`_}_pzsl#=8We70Gqubm38%N^p8PfHu%g2a}HAR1iWeL@ckk! z1SXC8`L7}?hGz-AvG5d8J$I1`zRGjOE};boyYW78NOoga3zV41L84cyrvtJ)uqkMv zr&@o%$j#>dM`Kk9(AuW^J=OE~)9h$gU*0cXqTXX0%k$b^alIJF9p{#~2lWsF7Pw$v z(*1q;ooU*a-nhG|hgw8>;9zWvI`E)y(TEP(CuW55=@?R*^MIFq2VrhD(z7L?vhQyZ z!^Ls+S&QgzhPa|t#@|RfveksYiEeoafL;i*`ED&>o?VEWW1gb2`D00Xy}IddV(~Bp zM#&219&ta~DKc!S1UJ=m_~-il;B1C^@Px<+(=X72UaF=&MiFCE>!AowZsvPnE_uV| zmdq@hRCbX!Y}TA{<0j0Sr86^o5>q-}iuIB7%B$5lmzk&iSP~G!AxXgB7*k^=GFQoD zRFAfh(7x!2eOz2FY1pvw<}scS@5}I|RI99%RL|C9B0n~6U=8-dt=&7!@Vdq&|j&2yLsxt z{p8kmQE%>tG}6g{RL|lAqFRV(_1bfy&rDu$w*yB|LIP51D(wJ!+Qj*JPlSig7nSn$ zbflW{JednNRr|cSn??)Ux~j(u;zSxZz{_Y`>GO6~Q{EO`^V*;fhk$l~ezcl*ker@k zwdSChZ|=N>@bO;HHwVS}5II1jBd7`L=ab2-T14ww*}g8B&5EaZXNr;o%4nd&7IY9AlM~ZaF9G0jK*WjXXK;#^P?K)< z>ev;jtBo~ph51jZPVhW`NbIFK>#2Q%fYs&JUCEI*+=i-R&e9(9?MjsKmM6HqXqyG;HCxTcrV z+xD{ymbAMINb&XG>^^UPttEarp$eKTXRMZ}Cm>y&DgS(vWoyXsl%YpbhCb2zXi905smpLCkr)3Dsj~hY0L(Bw z52+piB{a%=S|6s#r(7qMf!m@FR~t`?QgKMVaZ;@Gv8DW!cv}n)V(kNjtkf7QN5r_1 z`SgM@k)|-$a&*bvU7h_~IWS2Bh|htA=nb)uj*h4= zKNXkKcpylr7Jeo^ie!tJEx~j(`E!vLzl{p>D&p%^g=+YmDF2UApNosAYDG2~Cg(pF zyEPH*+3}@l5{+2bvDt2=s@^CHca>CEEvv1;Wng}(^ zyF@IT{;z%`!sMW&-*~=>VptU0jg2kk_lQvtkI{>7`;mN9t-&eht8_ z>XNgfFe4cG3x=!w5x*jJ&slMl1b|LHc{ZOD?})4`sd0C--;!kw&k@Vh>EOmXwedSK zV9pJ|zn)6c1#V(U$48Vq3R-ecc&EZ=wK1jG{L_!C*zZMV4wH+{wNmMAjv~)c7k@7f znKa)w=m!xp6#Sy~s=m%uwRBCu2=@hABf!jc1ip_VLeJcN%Ldy_$5Ps%TCP9ksl6aB z(j081rnKtFTF}EAGmsel4?6+@!QFL6XKs0-W-OY-%+^_2Oc_yW{}Baw5E`0R4wrc_ zmUm8lBZmz_-Xx%no7(^Q4<%Kr$NobBfu){z|08z#eX_@t{6Z5sswqV7N@Hosf>?53 z8GFV&G20C+;31nxfsFBCz4y~(~YtJl>}VKUx4a^=SB?J6l;jxnc0jhY`WbHrZH`fzy<406cf@J`Yd zG9(&IY(#||^oVS%xstECaYCc!f>PVNz%aX3o{ z>Z&2X{q`G4Z?yU*UZy1OB+RTCNbsv_XCox~p-Ss4$B7|oQD<4gq1@okGu8q1NM|`s zB&**#%e}OA?oE&b#mr{Hlb>jA^gYOQ1GJmEe<)OuOhw=x+>#1izgZO~$&?vE3+OL? z7W*f>$y92_8yl!;9@^J$L0N-<8AYLm!0*;wrEX7>-Nk9o{v>%6V#QkXfn<44XyXUO z9_$LuY^de)z^wW!fvS}*#v}KmBO&2g&Y~VwRZHh>{nEY?wSpmuV5c&`C0<>6hzv?% z@_0K_6eqBzP<#-fn|$?UhD;rd!(@C-!+$``=Z@zx8+>ss@KuYRev=7M>VaROlx&;hTXA$psf@03mTl1x@a0C@8qveo=AmFNl|MbGdLNzV6#b*1v)ff3R5`R*Wz2p$m05JU)_L5gh z;ZpDPkwYi{cMFhZa0ub^M$(26-G!ctPblaeN}5{GSLRM;GMIq@6S3X-(rv^{cY459 zq0=k@G4oyg+etIkd0ahEET2(-E|8i2mHgQP86n5QhSD7bDv7Q(e+l&TEwq8MtTDIP z%nO|xDa+^mB9*No`pe;^|3oX`)m7$8I=Ihsga@9*g+*)@nnNVrJdDXsmI9gr%)+2m-@$G*^>;^e-D;?8@mtE!@Un2nZ?KZ7sqns8)q=NMVQItlD--)e?vE^ku-;sBRi7QyP8m zBW98}VwyXgR4c8kY;fA9H8`s(^(K&o#>$8h0l&r@?>FZB%H3(k{sP9n zf7R`Uxy4hBRY@2g<5@*QBe8xZNl?Y(Xz;AcHBQ2RL%1e@6esJYy5|k?h*B`i86CIE70KVAx5IVrZ$I} z;7$psPCo7%Si_xt)VN9VX7kucXc_)!qd(%}dVKeWjz;7UX@A^+4~*zw00)iu^ud=z ztr?j6Cz$>pJYru@=S_(Fkm5}OwnaGx*B4`*2+#$fF%NJg3XTRS0pRV}DR{wwKMq&1 zSG@pv@SJtHuzCf+=Xm5(5bT9jBd-SFDgYk9JCWKAfYn5Q6yPy{?HZk&(CB0nW~X2KigyVP4# zXRoL>Z=(bcPwZFs%#btaXvFt3jhyQ5q_;DB_mvGgUkm2zB7R|e2lNf2 z0cz{*7paG4(yBP#^ZHB~?YHZ{pCvP5A_LaE`$ZhC8>1F3kz1|oxrT zN+vZET5mLYY7Nycr_W!ySH0knrPfYFoBIaiir3TBn4e^#8o5+19y#U_#-3p!(bbe20S^}6b6eU*A>nH-YV);jivbBtj&ZJVu8V!50> zs55m0dzY7!>t}`+h&NbuugN0tO8>lfDtD>(z-mnUQ!erM;ad)!H4a zC*V6WyZ}Vh%l>9dAFDC-99D2l7XYY3(RZ= z--!47(W`rr^5O-kfhJXPE;*k=Hy4;~1F?ws!;-TAd=t4jtqBOhy!pUKGeK!hgs z^J>>>nb-IM(k%1w`Z@F?+SEM0LDAzt64SuSb|Tf)djeSTf=Ob64+77Zppd;3*6&~A z@dE(N2*2Pn33keW;Kv0O;3xFx&Q)>iWS5vWGbYy4xJJhMT`jfSE#E0_rsm2hY8l{3 zD+U5lo7Ekp{Q~SZ#;#t!Qg#x()H_$oS^o97bgkT(Ya*2<(hjlwq$1am#wb*S*2&@I zx>68I%BiTQ&j3?Gh$%st0UbP~pprHWD^}|lkjET4j})6NKU2!joLklYMwuGM5^4=m zCG`~T{;p9bi@_?YNj^*K7ySHBn&b+t5uByV&=hdzk<*#oESxJrPyz<%2eiUr6gKzI z;E|`^S)_1k(Hk|3E|yd-H@|veUf+BVFWC_O3Yai`y=i33*HqLt(7{?KS5U8Skdfjy z<=r4Nasx)P->mzbDzXLcU*Artv_Hw-q}WRTBr`CW9H?CPXZcG%Zf-kTV*2h9}E;g9f~y-t>hkxaS`oc^PR{0#ZJJwq~l!#nXh z8L!T4kXBKnwr!FX=5q9)N4r6u)2!K;{!!}Z&9X^6_oODD57kVy{im(j3*}JW50vW- z(;Z~l(Q$s(d>^V3L#EBKf;yJ0ufS)H>fr)=Y1L|nofaOt&KV4Gaqu$IbY_`M-X%r^ z)7z=O!nK?}o1xQ3h<;EH8)RpFsopW9&D5aT>Ni975-UBuZjn>X^_teX61uPG*?g<) zEPJs|@q)}qR|TENCHI|_F@LxWA$gAZqZg#d4=LHB1S*R^9z|cOAWc_Sy^=&_2MS=? z`-Y67TGh@Kb`-!mO7okPm1(T?R7G254{?uLuvOA$AlXym5u}w}QZ9(6wRBvsQ3ua6~=cWQo17P!z*B74QY$*PQ7j6A%Z$fM3 zFpEO}71d6C1d;Nhx@x=3G!Fug48doU)U(^=7nj{aNPL?h=69ahR)RLrkAwJ;=mx76 z%$hwuw=!6WEh2h@b)m^*Hf&dSo;tIg!X@c<%Lws`%DY>R@~e>bcgu_}JO$fpitXx= zyJddT5lFpxnEQmS+s5(7qB4Bnq4b0|%NKlM-p)GX_z;wy1ULoo5df&8+bpd0bneP0 z$i{}EHx>=`%`O$DiKBnTscctN3pj+Di04xqACkcB1n3F6!SK>5S_~dt~}3E|}tf%}=TRC)Xf< zs#OONjpDzxmdgj@6V__>M>3IajenWFseQK0O>x7Ey6bOgDdVS8sJ>yGRV7W~V4|7afWd7Rrl1O2upU%5K(X zDyciv*1dB4qP(CZWxCfmios44eYMW~SQ3X@>v1EiJLyEd2WsCA(35~SuHA>4eN&Bt zWVfo^2joU8n^+6gBM-1M@+rH~X?P<5&12W3y|_n2~v)$0$+ z332b)ijLX_Zwz@WJmWj7CBx?Fvd=Vb>4-_HXY@nT9XeqEp&{dTKezxb!yi~Quic&` zQvJa=et!_}k=rg@Sfu{*Hwt+4QRR=xfx&CH`ZFlsPRqyPvEk}O$_4NdMzbqMXdxc&B|w+?ijnpJtO5W* z`V&aKp-w+8i>+LLwaW9z9@(s|F=_aIAP-GzK)zC~M=lWEJ+FFX7!K$0w(?hhm$yr6 z10?7(&-G8s^P*>Pd%2mg@uftU?r>EuFC&WJGMnPPU+sNX4vJ&jiRkj*Pg7@}m9~*= zha=G8?d;p*Ge01~CJWzP_Q%NBbB!d+9=A9U;L$gzY5Qe?-*vreKUqi)&;9#lls`sr z_<(#dw0Cd=JPN!sg9Au45pZ{+Jv#o>BOMJpst){v-0>Xs_CI8GtNSy&#rfZNa5?PF z!naE`|uf8rX9owGSXoPLj4#E?9 z+pvPUGrL|n-jEAQS*{*OSJ-?D<=ew_F1SLs^Cy+)!^!xFlkX1iabjo(n?ftquX8n< z+V{B(LodSBjsK)E`nzY`VY%KvRPP><)uGmVL8o0!J1)CsbFZ-R>JHpluH?VryoXyc zM12(_i#-R9%Vn}#u)jQ39d=bs@yhgGywQ9b?eNZWykDD@2R8UPTtZ*kQ#X5MqS&r> zdF7?1_j#43$`Z6JvnRdZm2u)-&xm*B$No+XeP6D%TFuR6_0-2QU943nK9)WE;ur)e zuQ$-_VSqOYc*CYu&}4Dgz1CUg6=ojINKfBSq*FHHP+e($R>xn}0*($li5FOCbuUuy z0=x(C1;81AB)mOKDLUM1PFMXL^1lPP0PrI~Sr?Q$kK|7PKLGp;0OolU>9lr3H9qOq zdcLwdW7@BNufr$hs^)Xq)tstz>c-FI0Fmr@?sM6TM47)75&AFrYe?gHwAl;*Ry0IX z>Z6-lY}3tK`g$ZXi7l3onr<<_IReI((N}S}x9@|xov6*Tk=H}$NAm+CFgVQR;~-L} zQ1~MP^yz#0RRY?hFR7mSyK_9|WZ)R~Kx(ShH%vZ3{Z9$_KL`PhYJL!c_3)E;6jB}i zx9o4u=sPf@`>7+vT88R#hPF7TdPbj-UI-5Y@B`1uz6D8W1S^r>2M2|E8s)b2!9l&d zm%6Gz>#Fvgqs4Nu`ujQA->WqQEC(a@xpbx4`ism+>Pys{AdjXD%eCxOj-Ghq zmN=GG)%pS`$3%zWSe<9qlpQ?K14b{9Mmb~t9tvG5W$g(_)NHzCopJzVQA8~M!i zOyF-O)aGoaW4_bO4{q~p#?^ctkB2cRjp)nO0MqdJCxF3t+5Q=*$E5a~ zeqNM(kbpOeK6h8SJjM5GBJGU88lm>pXgS96Ut+`)>b6MjAMl#7alA50`;b0=rY?!m zMyE|jL$d&wDmJ3o;@YVmiqSI0&cG`cDi$w3f4Kt`1Y*MeDa>KCne?iyc-N|!SZxs9 zffygFwTQ8c!KVrW{!^Ze2m8#eT48Z!Aznf++KgD8P&a)pvv{<^%mXaP)rxp+o=-qO zjMuu0v>QM*WM6*>shuF0yY{fkOwclaC+{A}dokS({ZoRr2z#B(1afL~TyeMpGp|4Rbn4dsmLMQ-gfon~u)iU^4?L^aY86f&x?RQ?E#$ zN9s?5x@X#wxL%1=Eyw)s(Pt_@Rhvv#23(Yt_5iwp;ctI7`Z@J*sy33s{@oc`( z{ff~ZS3px{%Yj@_isz>q0?_yzZKZ82&+b z?SP1%Nw~}O*U$o_SZ`Jj_0R^HU#^^~{?$WE8_$D02qk#py@Awm0{KxUpIB4;A&pGl zJ8EK2ZGve~9HpwJ%ym^QJ+&N>rQYtTy=I=Sq3`^x{G%nuN9)4UPtf9UtakT4SIoSnE9tT6tFPM~WA?AAk?8sCR%W zyHy(y=2Oh-a9?e74<7kqDoi709;T&J%&rRl38EnP-%*1LG~fBnK`Or7&*9^W_HGAw zo-NRZ_=4M^=9QqhLQ>d;Y(%rij#gKOHpqZFfbZs2ga(^Pks2dRKGxOs~({a8ljI4wEnFNy*ioR zbl@I+qMjL`%@p%IaRarCkU}PySu~3~~A#5X}AK@n!wPJ>^#a4XkbFocUuECCW8K%NzSO zzXu97LwweL3|}C{xk~;(lvmKNh!7p^*50{PjhtFCfd|oz)y_k;EK)d zcaD$0aF-*vuc}*#)_W%Ng~KIM{|z+!0`L`q*&KQ>Z#n52s)Pfu4A|+tdBNzW2W;bg z<3TG99kCp$c9f7A*R1|tqNNL^-Y(H5TPbh{J)AdI8<7-b5l;c;PMTUZR-5QwgkBu0 z^_UKJ;`fq34$t84!JIP$yov2YX0%;cZ|+BNcT=gg)?$K-zv1dsgXa59pl=9?d$?)l zH;mJ4V#|HEjnk~O?x{&c1xtZdeL9Jha+=DWtnF_M&RCwJAA+cp0H**x0)Q%|52o{W zU_kcuPw)!idA~n%0li?Ro6UeRjP47RJcqX|ZRT=;m5}~?zaeto)Rh~j5Mw{_{B?>J z8#0*a+GUNwF-64LcmAHv zI`D|PL-Jur1eRK|BZAYG8NYiV{Ly( zot&;^WqwW>9eo7-2g-MkF~@Mo3^GH)Jd0*%YkXSGw3pA$)E?AGP2YWqHj%EAeQ}AF z5orgP8qi9!ikquV8QV@Hx1;-Fl)M&TFeEF?6(jSJY1L&R9sX+Bw;=BEDq`C6@*2E<-TpI{kH37oLo zN&lOt2zH@|++ntg_mNQfcN7t@YGWZwir~46&4K8@kR~WX~4Qzn=HvIYMQycvEVfiW%)77kUt<+qEeH*eZ<=QckjIO(5 z``B9T#!}3&NZnnb%}c<`GQa2l4Uw+Ds-l)?+2%@rhBDlxCh4=5XuZWe)wo1^y*CRm zOZIoD!qlh1L)-}tp~HT@N2%47TCN#@30FHRiPLkHw^Dn9y3bpyFW9wLvhPK;ptlM? z+AkG6tM;mW4lTpP@`5_z&_;+QDrPCIFg#weKGb;9k*!h)GXcw#gY=p(t)rh|%GsYff58n&A`%PN zhS;OmY7fNtk*o_{8>)w{mG)RSZAPqZSTciu7=M{91V@{mQB8 ze@rH;rb}gls((a;$nBx3e;;WJ(KpzWjI)_ttW>CrD_K=sDl^rA`$XiJ4Q9%0z@wsSICA^}k;xaTljA*Fud)iE-d1eSr*H zOvZuFL)FTUWJXAB-XBm9mI(@6BaL4lOD|k&v1hFf^Y>j?)^y6XxQI#SUpI@MyUyzB z<$u1suTk=NLif0})UEAwCB@7amdh;mPV21et((QBIc92=Fuoa<9NR(d{)zvkb~k_R zRhHG-WtLSU-{#Iyb(d&e)Z{8M52mQ5DlNA$3evovTGxjo1&J*4#s>Ds%6S@+j22;e z8c2XX8j1T96jiFLp*TBzONe&3YU)|4X)-{i(Rn=n3S+rzO}>^_dU+wMe?D(@agc zvrGARByEkZIve4$agftS5B{yUVK%qbytv`+#w6Z>MNO(Ic{yHF(5aLAlFRBE!fj+H*yw_`K_lPp!23;K)VtzeLwx_i$^cj8|!LJ>0qnIYc+qtmcJ-OKMep&i^YngAIpJ> zSCAKxdJP~L$1&}EZb8hIpZ$17*OpK*q!12CQj`W>!&eADq zrZ}9Ee?~^6&TJGX@_HT58-65j`0>2CNAl*L=sn^@pTQ^kjXV=(>2ay}))LYqbL&NC zJ6UW!PS}c`5Jzl758H~)pNR*SGbxsoKD#U4Oz!tyO6J{>w?{TFJ>0kCaG$Y<$5tP% zx#D<@`$&!ZcunJxn#MEq19GB7(tjm#EdJ|0PJU;xgr<&{XQI@HE42O>k3)9$c)25KFK9QK+>^?l8^l<-+4y-%8p#HF{;mrjNN9J65_{w#MXRoK?jlzyvc@BBj zVo7qy&C^cCWgO04b0lt!N~zZB8_$0?$YRSsXA!A0#e3;FcTc^2>fzo~Uz+jKxWoC= zkEhRgGkwO-^a)sc`}22EmXvHNN$h?vF-=XqdFtWZDKC}0H1Kft#m5t;znM54CF$)2 z=QtAD7h+3nlOrq#3%gHBlP@L}Q~Kq!h{@K_mvd7li_n*cTPY{=6%k5VuUIvDcqJlY z@*wS%&MA`%wO0y5DQ!GAxQn`{TKktR7;yX*in0AUpS|kA8m+(Bqdu(BifD~T;7;|e z)gCmzJ!aJrt}y4PjbzrmKfVJ~t3z}TsJJ?NNWHC2!f>Rj2I^F@2CIHPxkU zwQfiKF6v({ZAc@}dTxZ>sGWEP_rV*7Zx7MA9oz<_t59@Ob18K9NfbB*5cECSvI)AU zQ7HEja9Eeo**LnG1%Jgn+Q3cMqrfEqSB&8#`iMhfe)kLnn7^6DPEe?;!nwk!v!}yO z4o9%yx9U#z!pv>ldKazJl2pdsGF<$s>ep(6%}B61uvUA!bPDQD1(*hq25=()I}Y!FabAFp$V1$K zQPY;8ukAt-nvl-wy>(hH{|t=YAI2tq=zxm)JB+M09qvmR?op;@BjR$U)>+lAC%4wL z_IIz>=ExO1MkmqYDFBSOc>;Gl=;0vP708=_M`i(2y90SF4@@_A;}hhSQn}v+P503o z_rL%pNueZ=5uH8TucC-V`yah50v3>N{^ah(c$`kaNn>moW3v1Ni0Msbeo;SNt#vie ztDf+rU89}1iqR_JI`Yu#)r{+CoKL8|*J<0V2*K&S)wJuip2Hpi%AJ_T$B>IHlQP~O z@y6m74ZeU)A1be?rtjWeMMb?~OR8vTp__>xsE4lC3azWDg8roX^m^@Lal0C{Ny{6} z(~lRpoj`sNAb5!603JD>fw&I^1L|s*RxQJ)J6$v@o#d3 zdb||(L5GW}!}&?AA|LD|e6Hee(C7+VsH(p~>uoIp=iXI2ZqVk?S5bbvLF;4sH181x z+79I{%t<$Di88sfr~F3kWi9xd3*)wERk9IuxxCn<-fzBRXi~_^Pn0G(}-d5I|Ec5|j`8<1>l8La^}i_QnE;7EYDuD*^a&Q5{lu z0Gz_xCV)M7#O`5-AOH7+;P(Qa#B0P1^n(B|18_9^C{hTa=$`=01war>KM#OVi2ftM zaRB(I`h7@+BlQAOFn;vC01p80+QcUyGpW-$PqR`;j{Y$~hjXzTW&^-E^6H!jArk-J z37Pdya<;Mda7f~JvLJNexy7n+tM+`@sL+eWxiL~*bi39kn)MuN=Fib*tEStvC#|#4 z>}6`q9TaZk`SYQ=@eVC3C8?4$dqKbk^!*GJeYI}S9kltAuU@@F%b@!dr|;0x8+#KD zZ(@V1s@hoqD-;W#Ipg#`K%a`|w31LVE2!tf`OjrUKxat*DR5)lO zf`}+CC@#@EUPVP*uDhsL#8E&6ui}pT<+}X!R?m{e_guZM zK0VLA829F5ad9ul#zuwEpMCWWOLygOi%Stn4|dKH;jwcEICGa(TbsrtS`R&**x(#c z5qHo)&5)3}gN%@|Ar);d52=WEh8{G1rGqOHE)SVIq@pdsqKZUY}6QPo!SzYNa zD@$)&6z&qDk99m^RCzC|DEkudM!B5snKd<4Gb?NJ!(=~{_eZ(0c!f9GSyfr#tSECk z*SO_CDs_bsu+!g0S%{jzP~Wb3d%#Ea?be3KwPKL9Ej+a`KI#$CFa$3Y5h%@%mT2D_ zEuF5)dgtg>&T6;pgU3Swh5@7i3X~bi!;j!(~7=xg~5xR%n@f;x4s*cTz#;bf=9-^k1 zBB}9#*f~Z9c;St&s&Q15xz<*fm$}>ycb!Ylr{bct%)fY;>S^;91k!7V4`(VYTkei zxaiPoVO)aa231){=gfEp}8^IVy9?qb z{#yOBdM53s>+d27Yz+AzEo*b=);4mG+Vq`~+PE!iMyPEaJ(26J&vJW?0|vPRU<(0n zsB=Z7+iO-it7U)`Zwz&^yr#O^S?;EOEK4B&jD zR9@QHscX0q(G#~3vc~G)y)*-I@J3WDDr596%ric{mA$RK-7gXm)(_p6T?|Q=-W@7i z=pm29=BnV0om^ACd@6NfCJ2@f(3{~TEms6xTpseB5uQ0pXw7-tdZfpyhI}fQP2kXyBy%nu+t*$BX*1heEF1oGQz!ztET2Yaq9e3**7(JSkma;>kuzoRBI_;Eao zT0z~?s3l|~neA;Y@qD`&ZFX*>()wOjPKONZNcSY+Z9LR#rZKh!$0p)cX4@`YVM>;G zQBDGwOn_C}XUVQZA%L>R_w5+P;|;gv+Y;JYTl;RxZVh`HUfK*$V|DM>IkmZb2Ut>$t=;=2}MC+l7O6#n+0b?d|eI7I5aelaF8xN|tQR_IsI{+sDcqE6@ zl~%=#nm0VjWmp>rCPn(^wocaJ0T=Pe9%fA)*e-b|-oA-|e{AD^m$hzSc62Y&A9*wG z2dV3eMQY=d1DA!8>C7)0k^tVVOK%|-KyL6|qJysF0K)T94(RJa8o zNc}O~vX0cyReq%IryHdYTOEdW>F_>XdPA#fYG?%0r7Mzv?j%9o&4X6$&2(Q^w|zvm7So%0LKrq;U+JF;o`*?fdCdy4BQRj-3{ARH z*2tDd`%|<4P+@g1&e2BuQ*E?Y6dx5C=s>=W=Y6fR`~s`O+t$RA^e+8?fO-EFu39tV zR9>HF<&>reU9K03Ibkqnl=!Y6sccA}e&_1LB>}OEpOoya|rl%CZ%eE>~ss60($aHt()3#Vsj zefvV|+s)Ho9>Ka`26e$3=c=u7S1w*#R$f&}zF@wodzAqZup*d*E8ukYwhqk5700a? zXJm^(jpt^}HY4}Y^9!PIMrMubDSowB1IO)9fC!;u+S-OYDRB4Ia8MnEt4NrN!2@H!!l?{Ot2v zZfoFtyX_}>>>6mjHY3?`&F{!oyN~tq{Omum+S?aoMgI`VYi@g|kgU@P;B)8~`OuBgdcz!Kp>q}9!9)6CO?*Ba+H#ne!JGW#HtG&5(_ z$mQWq*+bS2~S@gETmu{eTST$~0`desE zHZh@$UTX;0$r?g|A75j>(Jm?}r}grR{$h+Js(YowhZ&PEyzv!8A%>2Bc90p?r0Ub1 z%ISH2dySdd!lbeRZZbJo(`phMnBo#N%nBLPR39ZKDsQx_+EMFTTH`LOtk81+7^L-+ zES{$W%TFsFsPtDu8G`8 zn};=~)b=$xfPb=8ogR$F0CKGwnXUD0u-3Q6`{Z;pGiWW)e{XcHLy}D|t92}K5(_g{ zZI15`6wDW%t*2TKuj){Voh~f zAOQB8NW(i^?nQRyC2BS+O# zJAHGg*QOcb4XvddPqSQtTwtv@k!C%)v5nZ-_}a!kHXC#Quw}dQ2GP&jePyYw5S^Z5 zeRE~5c-u>rm8QgR8XwHr)D9zJ2{1ij8`1>y|Q$ z9(f}j6%~^lYisJ1mbgyOqt2N)a-gpML?z8+VW^`AY(+1qfrjsW?t&`=t&O%go2ta-QOL^GFmQl0vqtU$YO zNfSLAJ+~YIY4he$h_P+oKSbZVs75J;Jm7%57l89w_u=Xg0DBDg)79O&ExFygdjBNK z5fS zj|YDiVw&~!eW~#*9Ug;tM;^6O4rRx%ghI(Et(H$&C5J}laKB>%6KSQ}xgvlb?3%0M zLq(c(cH(qUB{?!=y&V-<xI5M8cFw^tq|hSx1i+ z58*Dcfq~aY;&QsD*Q_pK$9^B)>JGqG_74Id_DrgH+M4}Lp{TO9Ju^c*Z+-kswb*RU zeD;GutOJQ!2Ph({b~`GoDG=HP8uk$InvxiHH@B$=aTT(!@@Z?ubJ@aUZGEnz*l0CA z=hTC0y|wq3G^_CW1o1@Ugy%!Uh-aK+TKe)7Q#{)E=xfJA#nX+~z4fu^&Vz~d6xu9T zp|Nmsl*^Zsff$DNKSVimW45*L?U8L23Yp+gtr3@5KfK*ZH@v~hIo?G~Zk%v@sh~z5 z_?D7_ue?(e3$G&ISN*g#?nJh(eu`{FzPQy=CkFdT{QSgiW~^_1od#ku)Q2C4B}5G~8YERA%Eb>!^>$mp)D}>4!r22|9_1$ddgbO3=e^ z=bW#9grjb=As9ym_1)F9c#&d__*XmehIR43$XaM_^!emUF@q(Qs8RU|W}KQ6Oy*Iv zj^WQ-mQUe|V<)CHAW@(9Fu;mA*Fk(~^*(nkRAW7*NZ&m-0H!7W^R#|JO=yAoAh5Q? z1qRjIVLfBY9o6N|DkklM)$qBCa8ElWQmvRT-Y*OyW!km_)#Ct%3HYN%jsW1|1!p_U z{$-J{S@XY~=)=}{^viHDGKeln<9~-39##{?(O(ydcx&9(6@<8EE;$R&Q^B~_5Hnx7 z^Q;4Ql2}uUDJ6`wO4$n6k|yXUoF5>vtxe}w5_)!SW4y z--XA8>K-a0t%eKXq@Y(`$RbP!E?g#M_xtXZzwUAU{IYApNFYOoOFqFGu8fNzT0Dnzk%B*4k~ zf*{H2SQ|4^%=d&E;wzB_wek0MBfZ+Jw^f*kyNNnAG*om6g{f5=$C}BWhEQ=rgiohe zOXU!?z$OZoaEG~vEG3=~nZWm{d8w(S`u+^uKvdEk+mviZv3llP|CH?8x zViuLw_krA0RJdp_)~eia@hY(f)0`(VLR>H#-T^|SZsgmzqBqD(0DvCkXKHK63dA+N zsFZ}~AKT zve$U8iN<h$)QX{ zrE6SeMFp)Ran~`#^{7OBIgdb7yTS8F&|-j6bjz}Tou(?|MXc>E^0i!lRbMBHzUuyX zF^fb1L*=n2i1WgxMWEUzikt=qyo1LkSGauT+k?E|CCKhB^J^<6M?s$bJYqIlkukS;zJ4H*3r zUfSKJ<_|LoAV06=GD2_EEkj!i0OkPH10bl;ZUDOmb8)*KU;_YmH5S^@8vs6t8~{MG z`4Vzoa7nvEXvrRcc3o&|&PG{J9XxCBgaLCQF9B8_lsq2MwbqxiO7Vn&dOuVAP3J62 z)y^#OY+_Gf;^B^oP(DDw8qq&lU6Cz*X$a;fn%7{R_eRrdZNP`i10MDZ{sBBM0K5n= zi-0$-46CxdW&lr|dFD5kt)P}VrvXMcF_q*RgYzh@Z`ag24J6RMRK0V=Wi$kyTw$cD z*K$N-m%s$&jd#~9SyDy20od)t{(+`$mAX4u3>Jnun=87)?2>=x$&Eys687pwJ z`nx-c5h7fj>?De)KD-T2-_BxTw8-}C=p()t8HlJav?;Hts*!{3d5igPh};aG3{kiD z6Dy)QM9ZS?I5w%@KNo}5>%Y**t?4h$8MbRtZ?#%bC{zPSGaT2%Qe|%hFSRbJStDaW z+A!2yi7U=1a2|nq+9F`vhI#=ZdlQ;O=4CqgT8A16;3fdXPu?g;c@#EVlr3^s%T?;q zL88QV5Kr^eKL!cAn58})B)ZS%$$1x*d*cx8A%jKX17$4a&8WN^;6HdAR=%{ds-lTh zeG?!?Q46V3EJ_)wy2@M+CD6VxiOXz>*AEl%#*C}c;jI7x3CmjCLT70w4tBbXsjQ+M z13s(jwYh5QWVurYQ(8WY>eH*6)#(u;Q|wc}ju6{v>LD|_-xTGl-)ONlx0+BR%^6Na zae6hN3%<9r)S1zuU-1jXei!_KpEriwzT~coWBo=QalX}Q(nh^wBR@d`6R&r9Balj* ziaZ~yJ6qR>)a)^$uZ?wXi`q6uWaYtUY0}AbmI~J2s+=<@6^ibuH^+#9I=rh@ZN`dR zamdqWtQhSNSv}jvkydxu2~6w)+=Q!L%@J$vsSn1Bc8Lo~8opW5*S~vJ!~~HeUQz`U z#558GPck=55Vsq%k>ZuN5VDzLO7mm&THbBI-K1NnvL9vKv!KYfpzIES0|4l&f4aU) zZJSK;C{*`PrXlu*`eL&9$v@I4d9^8A+^QNb7SAo(N5r0EwZDz7c(ywi$qnN6c7Qto z4glN(9CzWkgzFiYhAA>*Y^%o0=E^RU9m@uj@jgvwoN z_-xTji{v_0H=F#-v!2^#i$^KR<)I|kZlj?Q9exDO{SCkaKt;3?i>to_fX>qVIN6Fx z)e|)1;Jnzwj16EjaQqEdY>uA?BJR**bman{$m_J*BYXqzDRt=r(LrQ+_Ad}=q%f2u zPF3%eiI4m9=wjrIgGU??`5bCW7V*9<-tGp$5nfrS&My?hM1R%GA@ts%RJGM1_SzX4 zlfifxV_*#7y-ADcy&`q>BGHqi4Ew8Qk1{em-z*X~VdLrLdG$-VC`jjpD;_OP5un%C zsNxEdCq^n~g`fzIyYhO4Slm9?mBU16kl0C1F;YxW*EvbG2dM|0;*wOfkN1FC<<-TQR$*} z@0LVg3!>Ype=iYnVup%XDk|6;pxfxeH*zd*F{Q%bia4i2MT@8{)aDNTJa{&JUxE5d% z8ktPMMT+3x^v6B~z-OvMrP!z!IeHqrsHUE&6nkcJ{;SCnwsrt~YYf2!jAm;LJkO`X zMo!`Fey*vG>L&X$=cc&>uSUb zL%i-ew^F<)3VB}Vc{4c4vMFh8^2F>b#{ta&^_fcyY~Oss^e+MKQiX0YSdaZVYK>cD zi3J|z7CXeKLBPPwu(gySiwd(~>pXr1T>zM)YF3H%+J}3^bImHTUf5u5%WZ1mJdxzd zT_X~VsS!YwM8z~`#a1|JE+)T+X66c4ep-`t%?woIgaRkfa?!|qqR|`M#5-`hWWKtr zUi1=E)x-5-fY`4-uNMQwIF+|fbhH(qPjgi1I?=5^y8*2s4azte`ub*;-s=8!B3T?% z&#w~~NS;rx7prw@<`W_(I94ZY5PjNVHOW8xG7B*L(&Qk|whiKDBaitic%_hbg&IA2 zc}1;6EESx|)Rc{4oOoC5+$j2IvXg{06giQAH@eE<(s@Hl6knx2+bA*`Hh_a|yrI?D z(VGwM3_BhLXb;y`Z%?Q&V{uB1&hSK%pJROlV8 z=Q6)I+~Y(6(-9NMH`YWhS|-26J1?ngH<7Xzss}d_{co!ao5Tm&V5O?>|01Skoupd+ zLWg;i*3-B(H3dnS|N!wZRPS$`ovIZP$GvHXn0J34Q`;T8HWCn1_ zXpsMJx(Wz@Ui7A@s>?-+a$HVU_8WD@C)))Dv68 zB?DMQh7NO(O};x+ufduwTF~W+;D|3*w9%&^S!&8vqOXWjmt7^gP_MUME#~&}VHycq z8I!4(`-4c>n6$tVqn2IGMk9cO2;|7ihXYY{6GB?U5vwL%LzaG!T6vA=8NUK-F-u7w zpsljMdf*x{uL0O3^HC=y@aWGn*1S5blZJ(8o&EXGfB?k!MxvwKUAYQNd_4KF=5l)+ zmm}vvd2d>RDs$J663c=vpel5H>6j^;9MX9i)=2gV*wRN)=j49%@K%x6kM&Tq4y0ZT zIyh9|+&&EA!iD5B&^!&g3aL(ghRV1`WQgxQU9J_sh$d<H2GFPTSyOrV-d2)*F<{WEVXZ^$Wk}%6#1gN z=g3ac2(OR6JMe7XC2kZc`-wp8bzV`k%E@tu*QE7lxmOL@EmGUIGzs!{wRpGaEas_e zcZ)6L+@|ai*PCLGy8Bj9UBEp7LLI<-h${>W+QR0K!s&AhvYmZZ@jfv~^i%8hiF7@6 zrmEfh$Sqsqd2*i^P9*A7X#4%*F(Zvp2C!-5Xv>?d+8hzdUG)%Vwt{)y(DItvwekZr zp9Su*4bH|@oa%OfEJ8mu<$%bJV|AxuGM%LBYW)GSwi`Up9S~-KfRFAJuNWOTg5lmV zQjp>`YiWBbi>VzjcT{iQBl3L9!I(e-`%9XdRH~MIkaT^w8h%i`fLV+Ee~Fv5aClos#(EQE}3UD5qCT$B+Se<#BS0n8ZT$)8nGN z6LTT4RW1Z1LH3P$Oy8XGc40O%|8;-EMo*tUWxDQUfNv7rz1#O;5<3>fU?5uqbTY6P zjno^AOho5$;4C8%(W9nXUARvq*qDfKJ^4>i+)ACqN|>kSNzqR%C;+;y0Nnt(1M~pk zT|FEL>?g!NUl^ZM=>4K)oWquPq0wIcms(Pjr^FG8Mcb-QN1(I3-0pe!h^P`Y`{x`L z-KO)Lx(i5xiWFd>HqHUAHA|=Sssl^dm-X>6V6fVIl*WICI(k&>r6#d>s@6RtPNZ=I zJX1DTQg1p7N^N{rMC)&9n9pGW6>8oa#Te4f5naClbPF9vt$Lkm)=> z^(Gm}J_IWcKQJz35uOeOz<6xXqX23u;{Y%S(Q((dP~Gdt|6Yd`jL!P_N!u;y7w*4p^=hjssrze z@6^L@i-o-LvNS)++h*$8Y3>0%rnzTTR?y~4mz+v%c-y#ZY9~=XwaQ=0Bb}EMSlsM* z@KpOEI0bLo9|;Es1~9RmXi4%olRmIHPLs>c>ha@ZxgLQ&;s%}&(;KGXO|(H2%4xV^ zT+?y&UsD3vH=~4OtUsj$N?qS6U<&oF&@vVAE(ND|sqA+}rH?odac+cI_P-|#VroVh z+vq|`VU0s#bj|vd4Z#WO$>Ct16dNm(aoo zP2a0QDV!PJ0oP=u$N^UIz~xyykitURUA_OM=&Zf(vnui{THdj#g0IAT)Hd%!$o^V{ z8~q!=j=ca#wMkw-#!51cPj0nEI*pe`*Qt5uMPYlaEBOPK22i*WV7t2Kygyk!M}2Uf z23#-o(|Pe0waW`@XTK%aD0>aHRv+v4FzsM5XZFrzpqusT{qIGehO2;Z9o3|-rm3gR zc0A(IpCW9_}RsEaj(Ftlo zqrvH$6?kpXCqpcUQDTCYQg^ANzlmb4I+ZH&cM8E5d3ydXZu7@Yk9=-?YTB}?tqZrR zm5YpCw2!qwU0+KR(bO=bAdOWi1G>q{S}dNj7-e_Qjxb}e(ZCwkzGV-QP1IV)`CIO5 zCa?taw-vbMRWFPZ>BKD$4346B(T5|!7%-iaFP%{ZEbzAXiLq9y$q`0^-h;Fnk9Voc z2xElq@G^COgpn)m^t>5i>_>-LX|Ibi+@Xb0z`7o9pznIbKv?y$(BD8#ziEmZZ|w67 zX=8L3@y7yusH5U?p|Btj3`Nw27>i zHiq-AL~jgPk;9!{SIrx0 z+1xJ#rY05|x7@SV)QN2ekCT|nYWmRCKU6R?G|4ptowqdbjc1e&B9ktmcHXv>W(nj7 zD3eD^-?Eb$o@Jz_V%HUa@ZjG`9c;f~*t_T? zSc=YabB37xKHdqL3+cw<>+oPU{Cl;pgOS#bqe6CgIg)w?Z4Fb$IvDAlz&UK$U5mwR zCw=Y7lad18IKJeZ`+k+s(MZb#`}~nSyc#k}G=y~i=m9mZqcJoEktKia4$0$B3xB8D z)6uYtGS8D8jqO;|pO$Zo?XZd}=L@zT^dG-USOaMgdRpI86&Dzp>fL;!t=O)<$T#f0 zv`=GdqZ5d`!F_sHW1%-g>;TrTC>y3u^)_<)2J@srPWwDX_G=N8vnz~Y@4K%ulG*vRt$ZPl?%2OV~tF6irT)-X@X@`dTzB1q#;m;(x3ldFZ zr&E1QVim{Ke+C+#`If+cFEr-n1&ohhRe2*StEz95+^p6QGBU+!wG01gRaI{cGG1!S zRPrQ=#3@^v`vx02PfID+uLl!@&kipEm#$fTOnpZ?qjBjJfT}8%lQJ9Wi#dV_(1?W@pYImihYOtL=kITRsjwU$7 z<_P%|b%etVuuEkRGsbE&XjfIkh>S1Q^}~$oeAIXb4L9bCo^}wzFF%GW#>H3<dlHzKFgx0CjnvaZa>0?QfxoY@W8f(6QWYbt_C`p` zQAii9-TvtSJfwz=H?lI>PzOg6F%^B+=6a!8JD!xki&Eo_ZF(O@SSkK!9X)bNEzY1J zcQ?PV{^1Zco>Z4RpfTL7A zPfRkR!+IvtZGK8{X9D?&`a9SaH5HC3y`ej}Md7geX__&OHXw|jZuFO1}@8i+V z9QDd{Bdv%nM~hp}`)BHpM)4B6DDPIgq+IjSZ*+~rFrqlv&C(h;8A8AfctiyuGJ z$c*E-g?3|BEiz8pI6kqdDdk4G7~)x8ZYZN2TqbX+9JAOv0*h_ z0Tnn-pftaatb@|i5}123eXqWvZp92b)~nUnnZp|lPHz;J$$a}XgR#x^z=|Dc{N*0m zmWo-@QOmK54*qBdH2K^vr>b3I^vK}o<`mcx!8l#CiMxC$Z|#gxk5P?utO?0Bco^sF zO_{4))>KtVw=$O+g=x**UvC8G9A?tO*D7_V;O zWXJ!6UczsZSKw;1>Q-xHChenZZ`Ane3g?>9SZCqcVt%dBThA6Bs2#Nwb-n31T5DWl zjyX=RpQF&~cfC=bdWN%USgi~SdgKXUW;gh1G;|mp-G{4p@Bq}n@Tv;8(Lb5(&D~TK z+<`|XN8Rr>io`k37j7fC!Wg;A_?HpA4v5gC{6SqqFpgIWzp53hV9a{OX{%U%RAn2;v~*KjHW)+vlf$VE#yK6c%2Mz_5FdhS0%BN-AeB}D z9c$!IC}XDlfGc)*ex@rwKj6I5!B}-tf1zLFYCc1O&-Gne7#1~Vqme55sD&GiCy6l5 z3>0oMmYd7KZf~fIvg<5P$ueM#bwNV$klJ^kgOf2F2YiWJ9-?323VRE^F*A4&!>^#Y zfNkWvc*wr=Iba&5Vy~d+=NHxE3L`Tw;8Oejz_V0`{dNBwcuG}YVRRKEJiDkqiI+2$ zZ~WExr6(ii)i7|4!t0Lk7w3a<6F~~1w4^z$~I%8c+_)jn-MK& zY482*MtXZ*Y`{FkXoLCjIEzO7O=_ZLr0cIvpxyp0$ec*vtlf81ij(PR{gn2svkiM6 z;ts_Tklyf>GAa7V5op&dt>F?`H}RknoEACLU#}sKULj#7Bycz zseMP4OAaD3o~Dhcu69)A@{o6vvUa-PO6ssV^4)~=oh4TG7Morv9!&MhsS5pBH_mIL zdj7@itx9e&JTtdZk#CVhAFjkU<1CfRf1-PMQ(F&=oV$JDXRs1`RP+S%fv?xSRq0-` z7|*Nvy~Z#$7(KFy)1@;hufoHxrD@m|q84tT81eLeBR0L9$N#J7R1~4z7;?hiVe_cB zV={e5NqR21)p*920B)=Cy$@JrA!ry` z*_NOZvmb)E+qawQW0ay5+JC2=q^^3N^~U)9BAkMyPl{;~J;{UW#r;Mnam@40eq)(v zDSnO(*oYoPBdz+(Da2n{MBq7ar7hL_`SOtOjj+g0-^Pi^jz8McBXhKzQIoc%wNSB% zz|4BKDWrie`vM;~%)!71kTd%WJe&R-w?6`~tH2H-D}E|ogC&%mfG`VJ9RYZ9;vrzi z16G+VTruIzyOc0Ok5^VMfI?Myz}Tt70NisJV87F#&sB2N;=7Cr<3~fia+k4P?aDEd zt?NID$XFjz7ES2+uR_orlEl6{wd-ypy~LnG_^rASck#{c;1luRk`Q_Zj`E+ov;V5X zRE{$=U1eAzE}bhS`5*Be#vA_Bz_Nm)NOguM|>wm-D+1mGDdofly z%8U%d-C5N(Q1txaK_kzG`GRtBQTH07#&!rsOnS@POE62*2zcXt0Tky*Yw?v)E}1)t z{_+W*dN6RtYrDGlUSo4-_J&}lz43t)cl^%fY`i-i$Y-c|_faVSky>}3F~oKfS~E-? zzR$2vhwDhMVyCLJ%zqq|OK0av8(FNT5`j|^x1($T0w;+Fh!6o^8`g46OissE*pzi#eu^?gv)uL~# zZd9=zBYOtB1y6t)1RHWc06UMhxLOHt8iGrKlpt9SPv|2w_$BgT05<>#l8@l(71iJ| z2HN=jI(67%bdElYa)d9g1UmLGDO^uZnP5<+>hvh(?y^0OM~!gohT~P@zdUB#WY{E> z<%}oi3FBwcCAeD5-VVN_b4w@1-laqgqG7GQhgI=YM*nu~1rbU9&#&mYH%y-yh_*&FDAKefd{C9Qqc;yVXklH8=uJZ1FIrvz|RsnrQ9nH)-%c}5&DVgiBw z;-`)0!is|{+L2(Kj`eZ1fk1tffAN_%USVafKJa;`ga3xeY*qCSqpHbgC2P?+_UNGE zO_n7Cp}Vi^c)Cu-zd%`3_6WACfiD=t1}MCL6FRD+XKy6>sN-gMY;q0B)*Gu=ru|)S zR0m%$x@(thnL70X$>=Yh)EA8?-)5&!b?jSOMjZOG@wFMvA+#^4G4OSYq8?I?*NxdD zTam(_ubwwg3+P4Gb?QH_8<&h>U28=5*dq($FJfs&u0vMvc|a6V;kY4RZUbxWt*qck zwdWW`c8#8Qj~SN+WVZ56qbh9VKCr<-4b;J`(`xxvT2FVbu^0Q!&DK(EF0XR_BYDxg zjzTq}4Gs3|KNr#J`sEtge|=I* zP#;1^dm{Zypgu9GqW6s1;hg@s)KpofnXQKZW`rjM>iOjQw&%_Fj5C4WzkbqKZ?mz+ z4pGz28X01(sy=IU>5h#OWC~vaL9YV5PQV+^$u4Jw*EU0+x6Xo(@H}(Ya2X9)SRa$0 z-TF~cz|m^=@gNbReFs-30Nw@o7~o3)Oq=p6x}t5lddSPKQGOoaCxGt((vnf{2VDLL z@EyR<0RK|2e?l2O4qCJ?>W?pigTNc3*MEImGhjN%IkjuQV|l20J;BFPYxwgv3JEAW zN$|}2)F^<>;xAcm{>=C_wBZL3!SgG4QwNvhqw5;&Q}ws&G6IFfr;v{}?qG$J;B+aS z1;gF+(Ni988?#Xq&j$+^0F7@GCga7ja+6kBp;?W~=s`1KRjJn!%s%w-y=ZIB zB@yuwU|n0YqsZO@(s|B1h%2^x+-=B%K4yqbHdEUEL9-iGrfJ&M)DEUiMJ5qT-%;sF z<^r8$^jJyec=3mAkbk4CJ7%Fcr8*^>!<*#z8j{WT4B=4oQq8nnw%SBKnd4~rC@A`S zbF$Vcm1^ed6MW97r&7%c^wCdLnwgu8tURBh-*Qji6NI6@P=^CdaaEexUVP%YJdGHj zw~cF0N5GpUdFD`3ux)wf^vMY2H0Zha^&jinm7tv`b zxZI;gcQO0x0~<@##x7>sD3*C2)ZlUWGOpetkRPM@#Ma|aX*>(8$JMDW=4c%*y-u(C zRd9ShsbH$A$~RxtwApm_&dgdT}WP?X6R)NS3(@k1Si`a)EX z9zE>S>YR1X3O>yO)J0Q09g;qVTOP5GG#KO1qa-0GDc<_O&tFr~hziB1>xG}A>l&&-}=Qb?z__983Zi@juy=-ER9 zuPLw$j#fr{21KE8`J&q0$IOYofy5!PJ|=V33sg`u6A$;J8rNm1KnzWO+rqa7dKRTk zD8@U$L?7$>&(Y??(xCaRghL-CeH^lEKivzwUdIndY44DH7>G7Gr}OiMWU4fUZp z&%ia8K8kknXpFl4YLTpB3(cXKn71i$%T3dx5T75NMaA`$kfM}~+B%4)o85(GTk%(Q zf1#N%9H}lksB{{-#av+dvOQvcFg0JGHTJ6lgHHcw!!Q*!$m}%qGd!OP6zqZcy!|-- zat!++`P?I~pdZ#CvRb5WW+2S9GhxE%xoeP_4Of@UNs{ON!RE8JG-d_&oVUF&KWxkh z^;oglbs#g1(<_Ef2mc$?oFhP4Y|2;Y&yHjWUI_zWop7j(k!D)z0Fy+QK)aMk#q|#d z&@b80%Jv>LWu%!QELAnqoM01p?UXt^(kyBhWD>gx%%Aouw!|FgSNM4)X6I@!)b~3@ zk_Yj5u;&W`-lUe9Hu_zSTD_jbeNCp;nyM2%vjyRQ_`M?Z^E1L&KST>RdX#Aw*WB4{ zlxd?Wvik%giIv4v>n4zqZm<46!F;MAIDmP?z7H%X0a|3_+m!1QO8t5*KSU*D-Te{F zkLVJ!UpE6TF*;7de}$*4i;?sW`Jehvr=jHcu$^`_e1jRTI!-d%iBp~-lgu`u@yvpk z@Gje?x76IJW?DNoqA#E{V65axwPmU~$L15~r&G^kbB{Tn7PL_;yyi}& zgvmkQ)TC)KzwIeXS{g8Y6mGnEB`4z(D8-yua~S69w6+p38rW~9jRygu99i(yFTc0D=*a;T5= zeKtJUPG?0KzC}T_;M>)S7O;K9;i{=_?&-b-FObov&@n-`&NBxRVqQ)^KhLZdUWuFZUoRn12}%DXC^2?{8Y6{xP41ZLvB#->jo%cn_m%fq8l^ zF63jNFQxt^?DrTw z#Lq1xc3LxR7F3a#$c z`s-2jdK6xFcT~TAU}Pk%52=V=ANXBL5wbQWKUQt5GAGj>wb!c5ywFHusQPk+nYtj0 zw6A`S-S-9VgTzq@9&I#O*;Ybd200f&|>+t%Vs9{)T&kAohcA<1K9lYf$lc6>RiwH^ec zIwy34htE-5y#x>pQuXe4+WucjIoGnZ74(5FtvEH)@~5HY{j~Zmtu<4_%jtDjiBOws z&0R6y*h1oy-f7eQ%{JXns5hk9IbwqOO-M-2L{r@%&00gBv87&cnV;nZqvQ)v)0S(? zcZPDr-WJ&*J@xp~Lzk-1IV869qFn& za?vU?URl*3MwZ>X)>b*{U#o(LDwo0^-cqaZ3%7Opxq#tT=ildH_e|I&Ooi| zIe^pAU2JFYI~Kvo7JeBmmOEU_U6QBeGeCvemwhf-OYgVpKg6r-4Q6j~jc3{h^A~f} zpK_Epq`?4tZM31GMmqh5cr3NiOkc{>woPV%IHC@3GUphB__xupi{UK)xEo2W7pUqD zz~1Cpy7I>PzSna3c1s|xlf$U1e%sP?d66Ugp6cq$%t+gtP{!`6VK2>Kk6lK237zXu zA75r3A2SYE#sjq2zrj=K+hFH$01hDF3F{@YaQ~hSJ89Ls2})dH<`wYc94a6W+fG&e z{YO!={%5MvE6kF-6Cf;wDsH3|L?gEU?#|zb(0}J`AxY`hpkpx^v`KJ4`-nJF5+Mg!}a&Dwj~bKjaAxhG^SgeTCUBjCA%?g6HUF za=2Um_8AK&lRBnmM$y*jqd+-*aIDZ41S-RzQl2q^c$3cQI@WDHAXgnD4R*{;o{^voN+h(3SA z7c=jve{MH>+3*#e^r$GyoFZ;ib1k#ea2}F}84KgZUy(L#4B^(>zOWGAoS+ z5Lh49V=-+OYvS2_J+AS*JK`WVB3gyc>Ubxps}C+ zA)(XGh2GZ|p$B698g+h;Ia$rwW#&c&HdGg>H+GvlIzaJDdFdP~4|*62$n$NEqom>e zyUmn|vWWOFgx2yn@csGnkROfk*nWEHd>B}C1^VFImlVJx@kkHzg$wOEyL4K8vB#WF ztQvlcnWJ~}3r-&A_xXw5_Hkr$CB>{Nj{Bm5~2yhB&hjnO4Ir|WKA3!B; zmjNsX-~*c*Lunt5{2Ly_q2?r?M7~XOt_BGgjvFK*dpQFDA({Lh;9G$20Nw(COD5qo z%206QDO_Q8m3IPQdXhYkm7(=42=JdM8*>8iG(f8(F$Y5f;4AF2h@h8=_3wtxc(;9y zwfO(CnvdD0qScGl;(g}0 z93D%sOWq`MXPiCQN5(AYtfs{sr|hgg-$zbw7ZrOOEd%7MuD6*P4Fy2p1egYZt&jSb zpu6LCKEN`7H30PhR{~rI@EX9I0G|SI)*%fD3IKWl3 0: + logger.info(f"Added {wishlist_added_count} failed tracks to wishlist from album '{self.album.name}'") + + except Exception as e: + logger.error(f"Error adding failed album tracks to wishlist: {e}") + # Determine the final message based on success or failure if self.permanently_failed_tracks: - final_message = f"Completed downloading {self.successful_downloads}/{len(self.missing_tracks)} missing album tracks!\n\nYou can now manually correct any failed downloads or close this window." + final_message = f"Completed downloading {self.successful_downloads}/{len(self.missing_tracks)} missing album tracks!\n\n" + + if wishlist_added_count > 0: + final_message += f"✨ Added {wishlist_added_count} failed track{'s' if wishlist_added_count != 1 else ''} to wishlist for automatic retry.\n\n" + + final_message += "You can also manually correct failed downloads or check the wishlist on the dashboard." # If there are failures, ensure the modal is visible and bring it to the front if self.isHidden(): diff --git a/ui/pages/dashboard.py b/ui/pages/dashboard.py index dcf91d1b..0691578b 100644 --- a/ui/pages/dashboard.py +++ b/ui/pages/dashboard.py @@ -1,7 +1,8 @@ from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame, QGridLayout, QScrollArea, QSizePolicy, QPushButton, - QProgressBar, QTextEdit, QSpacerItem, QGroupBox, QFormLayout, QComboBox) -from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject + QProgressBar, QTextEdit, QSpacerItem, QGroupBox, QFormLayout, QComboBox, + QDialog, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox) +from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject, QRunnable, QThreadPool from PyQt6.QtGui import QFont, QPalette, QColor import time import asyncio @@ -22,10 +23,447 @@ import io from core.matching_engine import MusicMatchingEngine from ui.components.database_updater_widget import DatabaseUpdaterWidget from core.database_update_worker import DatabaseUpdateWorker, DatabaseStatsWorker +from core.wishlist_service import get_wishlist_service from utils.logging_config import get_logger logger = get_logger("dashboard") +class DownloadMissingWishlistTracksModal(QDialog): + """Modal for downloading tracks from the wishlist with live progress tracking""" + process_finished = pyqtSignal() + + def __init__(self, wishlist_service, parent_dashboard, downloads_page, spotify_client, plex_client, soulseek_client): + super().__init__(parent_dashboard) + self.wishlist_service = wishlist_service + self.parent_dashboard = parent_dashboard + self.downloads_page = downloads_page + self.spotify_client = spotify_client + self.plex_client = plex_client + self.soulseek_client = soulseek_client + + # Import matching engine + self.matching_engine = MusicMatchingEngine() + + # State tracking + self.wishlist_tracks = [] + self.total_tracks = 0 + self.download_in_progress = False + self.cancel_requested = False + self.active_parallel_downloads = 0 + self.download_queue_index = 0 + self.completed_downloads = 0 + self.successful_downloads = 0 + self.failed_downloads = 0 + + # Track active downloads and failed tracks + self.active_downloads = [] + self.permanently_failed_tracks = [] + + # Parallel search tracking (adapted from sync.py) + self.parallel_search_tracking = {} + + self.setup_ui() + self.load_wishlist_tracks() + + def setup_ui(self): + """Setup the modal UI (simplified version based on sync.py modal)""" + self.setWindowTitle("Download Wishlist Tracks") + self.setMinimumSize(800, 600) + self.setStyleSheet(""" + DownloadMissingWishlistTracksModal { + background: #191414; + } + """) + + layout = QVBoxLayout(self) + layout.setContentsMargins(20, 20, 20, 20) + layout.setSpacing(15) + + # Header + header_label = QLabel("Download Missing Wishlist Tracks") + header_label.setFont(QFont("Arial", 16, QFont.Weight.Bold)) + header_label.setStyleSheet("color: #ffffff;") + + # Info label + self.info_label = QLabel("Loading wishlist tracks...") + self.info_label.setFont(QFont("Arial", 11)) + self.info_label.setStyleSheet("color: #b3b3b3;") + + # Track table (simplified) + self.track_table = QTableWidget() + self.track_table.setColumnCount(4) + self.track_table.setHorizontalHeaderLabels(["Track", "Artist", "Retry Count", "Status"]) + self.track_table.horizontalHeader().setStretchLastSection(True) + self.track_table.setStyleSheet(""" + QTableWidget { + background: #282828; + border: 1px solid #404040; + border-radius: 8px; + gridline-color: #404040; + } + QTableWidget::item { + padding: 8px; + border-bottom: 1px solid #404040; + } + QHeaderView::section { + background: #404040; + color: #ffffff; + padding: 8px; + border: none; + font-weight: bold; + } + """) + + # Progress bar + self.download_progress = QProgressBar() + self.download_progress.setVisible(False) + self.download_progress.setStyleSheet(""" + QProgressBar { + border: 1px solid #404040; + border-radius: 8px; + text-align: center; + background: #282828; + } + QProgressBar::chunk { + background: #1db954; + border-radius: 7px; + } + """) + + # Buttons + button_layout = QHBoxLayout() + + self.begin_download_btn = QPushButton("🚀 Begin Downloads") + self.begin_download_btn.setFixedHeight(40) + self.begin_download_btn.clicked.connect(self.start_downloads) + self.begin_download_btn.setStyleSheet(""" + QPushButton { + background: #1db954; + border: none; + border-radius: 20px; + color: #000000; + font-size: 12px; + font-weight: bold; + padding: 8px 20px; + } + QPushButton:hover { + background: #1ed760; + } + QPushButton:disabled { + background: #404040; + color: #888888; + } + """) + + self.cancel_btn = QPushButton("Cancel") + self.cancel_btn.setFixedHeight(40) + self.cancel_btn.clicked.connect(self.on_cancel_clicked) + self.cancel_btn.setStyleSheet(""" + QPushButton { + background: #404040; + border: none; + border-radius: 20px; + color: #ffffff; + font-size: 12px; + padding: 8px 20px; + } + QPushButton:hover { + background: #505050; + } + """) + + self.clear_wishlist_btn = QPushButton("🗑️ Clear Wishlist") + self.clear_wishlist_btn.setFixedHeight(40) + self.clear_wishlist_btn.clicked.connect(self.clear_wishlist) + self.clear_wishlist_btn.setStyleSheet(""" + QPushButton { + background: #e22134; + border: none; + border-radius: 20px; + color: #ffffff; + font-size: 12px; + padding: 8px 20px; + } + QPushButton:hover { + background: #ff4757; + } + """) + + button_layout.addStretch() + button_layout.addWidget(self.clear_wishlist_btn) + button_layout.addWidget(self.cancel_btn) + button_layout.addWidget(self.begin_download_btn) + + # Add to layout + layout.addWidget(header_label) + layout.addWidget(self.info_label) + layout.addWidget(self.track_table) + layout.addWidget(self.download_progress) + layout.addLayout(button_layout) + + def load_wishlist_tracks(self): + """Load tracks from wishlist""" + try: + self.wishlist_tracks = self.wishlist_service.get_wishlist_tracks_for_download() + self.total_tracks = len(self.wishlist_tracks) + + if self.total_tracks == 0: + self.info_label.setText("No tracks in wishlist") + self.begin_download_btn.setEnabled(False) + return + + self.info_label.setText(f"Found {self.total_tracks} tracks in wishlist ready for retry") + + # Populate table + self.track_table.setRowCount(self.total_tracks) + + for i, track_data in enumerate(self.wishlist_tracks): + # Track name + self.track_table.setItem(i, 0, QTableWidgetItem(track_data.get('name', 'Unknown Track'))) + + # Artist + artists = track_data.get('artists', []) + artist_name = artists[0].get('name', 'Unknown Artist') if artists else 'Unknown Artist' + self.track_table.setItem(i, 1, QTableWidgetItem(artist_name)) + + # Retry count + retry_count = track_data.get('retry_count', 0) + self.track_table.setItem(i, 2, QTableWidgetItem(str(retry_count))) + + # Status + self.track_table.setItem(i, 3, QTableWidgetItem("Pending")) + + except Exception as e: + logger.error(f"Error loading wishlist tracks: {e}") + self.info_label.setText(f"Error loading tracks: {str(e)}") + + def start_downloads(self): + """Start downloading all wishlist tracks""" + try: + if self.total_tracks == 0: + return + + self.download_in_progress = True + self.cancel_requested = False + self.begin_download_btn.setEnabled(False) + self.download_progress.setVisible(True) + self.download_progress.setMaximum(self.total_tracks) + self.download_progress.setValue(0) + + # Start parallel downloads (simplified approach) + self.active_parallel_downloads = 0 + self.download_queue_index = 0 + self.completed_downloads = 0 + self.successful_downloads = 0 + self.failed_downloads = 0 + + # Initialize tracking + self.active_downloads = [] + self.permanently_failed_tracks = [] + self.parallel_search_tracking = {} + + self.start_next_batch_of_downloads() + + except Exception as e: + logger.error(f"Error starting downloads: {e}") + QMessageBox.critical(self, "Error", f"Failed to start downloads: {str(e)}") + + def start_next_batch_of_downloads(self, max_concurrent=3): + """Start the next batch of downloads up to the concurrent limit""" + while (self.active_parallel_downloads < max_concurrent and + self.download_queue_index < len(self.wishlist_tracks)): + track_data = self.wishlist_tracks[self.download_queue_index] + track_index = self.download_queue_index + + # Update UI + self.track_table.setItem(track_index, 3, QTableWidgetItem("🔍 Searching...")) + + # Start search and download for this track (simplified) + self.search_and_download_track_simple(track_data, self.download_queue_index) + + self.active_parallel_downloads += 1 + self.download_queue_index += 1 + + if (self.download_queue_index >= len(self.wishlist_tracks) and self.active_parallel_downloads == 0): + self.on_all_downloads_complete() + + def search_and_download_track_simple(self, track_data, download_index): + """Simplified search and download for wishlist tracks""" + try: + # Create a simple search worker + artist_name = track_data.get('artists', [{}])[0].get('name', '') if track_data.get('artists') else '' + track_name = track_data.get('name', '') + + if not track_name: + self.on_track_download_failed(download_index, "Missing track name") + return + + # Create search query + query = f"{artist_name} {track_name}".strip() + if not query: + self.on_track_download_failed(download_index, "Cannot create search query") + return + + # Use a simple approach - directly call soulseek search + worker = SimpleWishlistDownloadWorker(self.soulseek_client, query, track_data, download_index) + worker.signals.download_completed.connect(self.on_track_download_completed) + worker.signals.download_failed.connect(self.on_track_download_failed) + + QThreadPool.globalInstance().start(worker) + + except Exception as e: + logger.error(f"Error starting track download: {e}") + self.on_track_download_failed(download_index, str(e)) + + def on_track_download_completed(self, download_index, download_id): + """Handle successful track download""" + try: + track_data = self.wishlist_tracks[download_index] + track_id = track_data.get('spotify_track_id') + + # Update UI + self.track_table.setItem(download_index, 3, QTableWidgetItem("✅ Downloaded")) + + # Mark as successful in wishlist service + if track_id: + self.wishlist_service.mark_track_download_result(track_id, success=True) + + self.successful_downloads += 1 + self.completed_downloads += 1 + self.active_parallel_downloads -= 1 + + # Update progress + self.download_progress.setValue(self.completed_downloads) + + # Continue with next downloads + self.start_next_batch_of_downloads() + + except Exception as e: + logger.error(f"Error handling download completion: {e}") + + def on_track_download_failed(self, download_index, error_message): + """Handle failed track download""" + try: + track_data = self.wishlist_tracks[download_index] + track_id = track_data.get('spotify_track_id') + + # Update UI + self.track_table.setItem(download_index, 3, QTableWidgetItem("❌ Failed")) + + # Mark as failed in wishlist service (increment retry count) + if track_id: + self.wishlist_service.mark_track_download_result(track_id, success=False, error_message=error_message) + + self.failed_downloads += 1 + self.completed_downloads += 1 + self.active_parallel_downloads -= 1 + + # Update progress + self.download_progress.setValue(self.completed_downloads) + + # Continue with next downloads + self.start_next_batch_of_downloads() + + except Exception as e: + logger.error(f"Error handling download failure: {e}") + + def on_all_downloads_complete(self): + """Handle completion of all downloads""" + try: + self.download_in_progress = False + + # Show completion message + message = f"Wishlist processing complete!\n\n" + message += f"Successfully downloaded: {self.successful_downloads}\n" + message += f"Failed: {self.failed_downloads}\n" + message += f"Total processed: {self.completed_downloads}\n\n" + + if self.failed_downloads > 0: + message += "Failed tracks remain in wishlist for future retry." + else: + message += "All tracks downloaded successfully!" + + QMessageBox.information(self, "Downloads Complete", message) + + # Emit signal to update parent + self.process_finished.emit() + + # Close modal + self.accept() + + except Exception as e: + logger.error(f"Error handling downloads completion: {e}") + + def clear_wishlist(self): + """Clear all tracks from wishlist""" + try: + reply = QMessageBox.question( + self, "Clear Wishlist", + "Are you sure you want to clear all tracks from the wishlist?\n\nThis action cannot be undone.", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + + if reply == QMessageBox.StandardButton.Yes: + if self.wishlist_service.clear_wishlist(): + QMessageBox.information(self, "Wishlist Cleared", "All tracks have been removed from the wishlist.") + self.process_finished.emit() # Update parent count + self.accept() # Close modal + else: + QMessageBox.warning(self, "Error", "Failed to clear wishlist.") + except Exception as e: + logger.error(f"Error clearing wishlist: {e}") + QMessageBox.critical(self, "Error", f"Failed to clear wishlist: {str(e)}") + + def on_cancel_clicked(self): + """Handle cancel button""" + self.cancel_requested = True + self.process_finished.emit() + self.reject() + + +class SimpleWishlistDownloadWorker(QRunnable): + """Simple worker to download a single wishlist track""" + + class Signals(QObject): + download_completed = pyqtSignal(int, str) # download_index, download_id + download_failed = pyqtSignal(int, str) # download_index, error_message + + def __init__(self, soulseek_client, query, track_data, download_index): + super().__init__() + self.soulseek_client = soulseek_client + self.query = query + self.track_data = track_data + self.download_index = download_index + self.signals = self.Signals() + + def run(self): + """Run the download""" + try: + # Get quality preference + from config.settings import config_manager + quality_preference = config_manager.get_quality_preference() + + # Use async method in sync context + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + try: + download_id = loop.run_until_complete( + self.soulseek_client.search_and_download_best(self.query, quality_preference) + ) + + if download_id: + self.signals.download_completed.emit(self.download_index, download_id) + else: + self.signals.download_failed.emit(self.download_index, "No search results found") + + finally: + loop.close() + + except Exception as e: + self.signals.download_failed.emit(self.download_index, str(e)) + + class MetadataUpdateWorker(QThread): """Worker thread for updating Plex artist metadata using Spotify data""" progress_updated = pyqtSignal(str, int, int, float) # current_artist, processed, total, percentage @@ -562,9 +1000,9 @@ class DashboardDataProvider(QObject): def set_service_clients(self, spotify_client, plex_client, soulseek_client): self.service_clients = { - 'spotify': spotify_client, - 'plex': plex_client, - 'soulseek': soulseek_client + 'spotify_client': spotify_client, + 'plex_client': plex_client, + 'soulseek_client': soulseek_client } def set_page_references(self, downloads_page, sync_page): @@ -1214,15 +1652,43 @@ class DashboardPage(QWidget): # Initialize list to track active stats workers self._active_stats_workers = [] + # Initialize wishlist service and timers + self.wishlist_service = get_wishlist_service() + + # Timer for updating wishlist button count + self.wishlist_update_timer = QTimer() + self.wishlist_update_timer.timeout.connect(self.update_wishlist_button_count) + self.wishlist_update_timer.start(30000) # Update every 30 seconds + + # Timer for automatic wishlist retry processing + self.wishlist_retry_timer = QTimer() + self.wishlist_retry_timer.timeout.connect(self.process_wishlist_automatically) + self.wishlist_retry_timer.start(3600000) # Process every hour (3600000 ms) + + # Track if automatic processing is currently running + self.auto_processing_wishlist = False + # Load initial database statistics (with delay to avoid startup issues) QTimer.singleShot(1000, self.refresh_database_statistics) + # Load initial wishlist count (with slight delay) + QTimer.singleShot(1500, self.update_wishlist_button_count) - def set_service_clients(self, spotify_client, plex_client, soulseek_client): + def set_service_clients(self, spotify_client, plex_client, soulseek_client, downloads_page=None): """Called from main window to provide service client references""" self.data_provider.set_service_clients(spotify_client, plex_client, soulseek_client) + + # Store service clients for wishlist modal + self.service_clients = { + 'spotify_client': spotify_client, + 'plex_client': plex_client, + 'soulseek_client': soulseek_client, + 'downloads_page': downloads_page + } def set_page_references(self, downloads_page, sync_page): """Called from main window to provide page references for live data""" + self.downloads_page = downloads_page + self.sync_page = sync_page self.data_provider.set_page_references(downloads_page, sync_page) def set_app_start_time(self, start_time): @@ -1302,9 +1768,15 @@ class DashboardPage(QWidget): def create_header(self): header = QWidget() - layout = QVBoxLayout(header) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) + main_layout = QHBoxLayout(header) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(20) + + # Left side - Title and subtitle + left_widget = QWidget() + left_layout = QVBoxLayout(left_widget) + left_layout.setContentsMargins(0, 0, 0, 0) + left_layout.setSpacing(5) # Welcome message welcome_label = QLabel("System Dashboard") @@ -1316,8 +1788,52 @@ class DashboardPage(QWidget): subtitle_label.setFont(QFont("Arial", 14)) subtitle_label.setStyleSheet("color: #b3b3b3;") - layout.addWidget(welcome_label) - layout.addWidget(subtitle_label) + left_layout.addWidget(welcome_label) + left_layout.addWidget(subtitle_label) + + # Right side - Wishlist button + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.setSpacing(0) + + # Spacer to align button with title + right_layout.addStretch() + + # Wishlist button + self.wishlist_button = QPushButton("🎵 Wishlist (0)") + self.wishlist_button.setFixedHeight(45) + self.wishlist_button.setFixedWidth(150) + self.wishlist_button.clicked.connect(self.on_wishlist_button_clicked) + self.wishlist_button.setStyleSheet(""" + QPushButton { + background: #1db954; + border: none; + border-radius: 22px; + color: #000000; + font-size: 12px; + font-weight: bold; + padding: 8px 16px; + } + QPushButton:hover { + background: #1ed760; + } + QPushButton:pressed { + background: #169c46; + } + QPushButton:disabled { + background: #404040; + color: #666666; + } + """) + + right_layout.addWidget(self.wishlist_button) + right_layout.addStretch() + + # Add to main layout + main_layout.addWidget(left_widget) + main_layout.addStretch() # Push button to the right + main_layout.addWidget(right_widget) return header @@ -1881,6 +2397,13 @@ class DashboardPage(QWidget): def closeEvent(self, event): """Clean up threads when dashboard is closed""" self.cleanup_threads() + + # Stop wishlist timers + if hasattr(self, 'wishlist_update_timer'): + self.wishlist_update_timer.stop() + if hasattr(self, 'wishlist_retry_timer'): + self.wishlist_retry_timer.stop() + super().closeEvent(event) def cleanup_threads(self): @@ -1892,6 +2415,261 @@ class DashboardPage(QWidget): thread.wait(1000) # Wait up to 1 second thread.deleteLater() self.data_provider._test_threads.clear() + + def on_wishlist_button_clicked(self): + """Handle wishlist button click - open wishlist modal""" + try: + summary = self.wishlist_service.get_wishlist_summary() + total_tracks = summary['total_tracks'] + + if total_tracks == 0: + QMessageBox.information(self, "Wishlist", "Your wishlist is empty!\n\nFailed download tracks will be automatically added here for retry.") + return + + # Need to get service clients to pass to modal + if not hasattr(self, 'service_clients'): + QMessageBox.warning(self, "Wishlist", "Service clients not initialized. Please restart the application.") + return + + spotify_client = self.service_clients.get('spotify_client') + plex_client = self.service_clients.get('plex_client') + soulseek_client = self.service_clients.get('soulseek_client') + downloads_page = self.downloads_page + + if not all([spotify_client, plex_client, soulseek_client, downloads_page]): + QMessageBox.warning(self, "Wishlist", "Required services not available. Please check your service connections.") + return + + # Create and show the wishlist download modal + modal = DownloadMissingWishlistTracksModal( + self.wishlist_service, + self, + downloads_page, + spotify_client, + plex_client, + soulseek_client + ) + modal.process_finished.connect(self.update_wishlist_button_count) # Update count when done + modal.exec() + + except Exception as e: + logger.error(f"Error opening wishlist: {e}") + QMessageBox.critical(self, "Error", f"Failed to open wishlist: {str(e)}") + + def update_wishlist_button_count(self): + """Update the wishlist button with current count""" + try: + count = self.wishlist_service.get_wishlist_count() + + if hasattr(self, 'wishlist_button'): + self.wishlist_button.setText(f"🎵 Wishlist ({count})") + + # Enable/disable button based on count + if count == 0: + self.wishlist_button.setStyleSheet(""" + QPushButton { + background: #404040; + border: none; + border-radius: 22px; + color: #888888; + font-size: 12px; + font-weight: bold; + padding: 8px 16px; + } + QPushButton:hover { + background: #505050; + color: #999999; + } + """) + else: + self.wishlist_button.setStyleSheet(""" + QPushButton { + background: #1db954; + border: none; + border-radius: 22px; + color: #000000; + font-size: 12px; + font-weight: bold; + padding: 8px 16px; + } + QPushButton:hover { + background: #1ed760; + } + QPushButton:pressed { + background: #169c46; + } + """) + except Exception as e: + logger.error(f"Error updating wishlist button count: {e}") + + def process_wishlist_automatically(self): + """Automatically process wishlist tracks in the background""" + try: + # Skip if already processing or no service clients available + if self.auto_processing_wishlist: + logger.debug("Wishlist auto-processing already running, skipping") + return + + if not hasattr(self, 'service_clients') or not self.service_clients: + logger.debug("Service clients not available for wishlist auto-processing") + return + + # Check if we have tracks to process + wishlist_count = self.wishlist_service.get_wishlist_count() + if wishlist_count == 0: + logger.debug("No tracks in wishlist for auto-processing") + return + + # Get service clients + spotify_client = self.service_clients.get('spotify_client') + plex_client = self.service_clients.get('plex_client') + soulseek_client = self.service_clients.get('soulseek_client') + downloads_page = self.downloads_page + + if not all([spotify_client, plex_client, soulseek_client, downloads_page]): + logger.warning("Required services not available for wishlist auto-processing") + return + + logger.info(f"Starting automatic wishlist processing for {wishlist_count} tracks") + self.auto_processing_wishlist = True + + # Create and run the background processing worker + worker = AutoWishlistProcessorWorker( + self.wishlist_service, + spotify_client, + plex_client, + soulseek_client, + downloads_page + ) + worker.signals.processing_complete.connect(self.on_auto_wishlist_processing_complete) + worker.signals.processing_error.connect(self.on_auto_wishlist_processing_error) + + # Run in thread pool + QThreadPool.globalInstance().start(worker) + + except Exception as e: + logger.error(f"Error starting automatic wishlist processing: {e}") + self.auto_processing_wishlist = False + + def on_auto_wishlist_processing_complete(self, successful, failed, total): + """Handle completion of automatic wishlist processing""" + try: + self.auto_processing_wishlist = False + + logger.info(f"Automatic wishlist processing complete: {successful} successful, {failed} failed, {total} total") + + # Update button count since tracks may have been removed + self.update_wishlist_button_count() + + # Show toast notification if there were successful downloads + if successful > 0 and hasattr(self, 'toast_manager') and self.toast_manager: + message = f"Found {successful} wishlist track{'s' if successful != 1 else ''} automatically!" + self.toast_manager.success(message) + + except Exception as e: + logger.error(f"Error handling automatic wishlist processing completion: {e}") + + def on_auto_wishlist_processing_error(self, error_message): + """Handle error in automatic wishlist processing""" + try: + self.auto_processing_wishlist = False + logger.error(f"Automatic wishlist processing failed: {error_message}") + except Exception as e: + logger.error(f"Error handling automatic wishlist processing error: {e}") + + +class AutoWishlistProcessorWorker(QRunnable): + """Background worker for automatic wishlist processing""" + + class Signals(QObject): + processing_complete = pyqtSignal(int, int, int) # successful, failed, total + processing_error = pyqtSignal(str) # error_message + + def __init__(self, wishlist_service, spotify_client, plex_client, soulseek_client, downloads_page): + super().__init__() + self.wishlist_service = wishlist_service + self.spotify_client = spotify_client + self.plex_client = plex_client + self.soulseek_client = soulseek_client + self.downloads_page = downloads_page + self.signals = self.Signals() + + def run(self): + """Run automatic wishlist processing""" + try: + # Get quality preference + from config.settings import config_manager + quality_preference = config_manager.get_quality_preference() + + # Get wishlist tracks (limit to prevent overwhelming the system) + wishlist_tracks = self.wishlist_service.get_wishlist_tracks_for_download(limit=10) + + if not wishlist_tracks: + self.signals.processing_complete.emit(0, 0, 0) + return + + total_tracks = len(wishlist_tracks) + successful_downloads = 0 + failed_downloads = 0 + + logger.info(f"Processing {total_tracks} wishlist tracks automatically") + + # Process each track + for track_data in wishlist_tracks: + try: + # Create search query + artist_name = track_data.get('artists', [{}])[0].get('name', '') if track_data.get('artists') else '' + track_name = track_data.get('name', '') + + if not track_name: + failed_downloads += 1 + continue + + query = f"{artist_name} {track_name}".strip() + if not query: + failed_downloads += 1 + continue + + # Attempt download + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + try: + download_id = loop.run_until_complete( + self.soulseek_client.search_and_download_best(query, quality_preference) + ) + + track_id = track_data.get('spotify_track_id') + + if download_id and track_id: + # Mark as successful (removes from wishlist) + self.wishlist_service.mark_track_download_result(track_id, success=True) + successful_downloads += 1 + logger.info(f"Auto-downloaded wishlist track: '{track_name}' by {artist_name}") + else: + # Mark as failed (increment retry count) + if track_id: + self.wishlist_service.mark_track_download_result(track_id, success=False, error_message="No search results found") + failed_downloads += 1 + + finally: + loop.close() + + except Exception as e: + logger.error(f"Error processing wishlist track '{track_name}': {e}") + + # Mark as failed + track_id = track_data.get('spotify_track_id') + if track_id: + self.wishlist_service.mark_track_download_result(track_id, success=False, error_message=str(e)) + failed_downloads += 1 + + # Emit completion + self.signals.processing_complete.emit(successful_downloads, failed_downloads, total_tracks) + + except Exception as e: + logger.error(f"Critical error in automatic wishlist processing: {e}") + self.signals.processing_error.emit(str(e)) # Stop the data provider timers if hasattr(self.data_provider, 'download_stats_timer'): diff --git a/ui/pages/sync.py b/ui/pages/sync.py index cbe6e175..56965c44 100644 --- a/ui/pages/sync.py +++ b/ui/pages/sync.py @@ -14,9 +14,13 @@ from core.soulseek_client import TrackResult import re import asyncio from core.matching_engine import MusicMatchingEngine +from core.wishlist_service import get_wishlist_service from ui.components.toast_manager import ToastType from database.music_database import get_database from core.plex_scan_manager import PlexScanManager +from utils.logging_config import get_logger + +logger = get_logger("sync") # Define constants for storage STORAGE_DIR = "storage" @@ -3574,6 +3578,8 @@ class DownloadMissingTracksModal(QDialog): self.parent_sync_page = parent_page # Reference to sync page for scan manager self.downloads_page = downloads_page self.matching_engine = MusicMatchingEngine() + self.wishlist_service = get_wishlist_service() + # State tracking self.total_tracks = len(playlist.tracks) self.matched_tracks_count = 0 @@ -4461,9 +4467,46 @@ class DownloadMissingTracksModal(QDialog): if self.successful_downloads > 0 and hasattr(self, 'parent_sync_page') and self.parent_sync_page.scan_manager: self.parent_sync_page.scan_manager.request_scan(f"Playlist download completed ({self.successful_downloads} tracks)") + # Add permanently failed tracks to wishlist before showing completion message + failed_count = len(self.permanently_failed_tracks) + wishlist_added_count = 0 + + if self.permanently_failed_tracks: + try: + # Add failed tracks to wishlist + source_context = { + 'playlist_name': getattr(self.playlist, 'name', 'Unknown Playlist'), + 'playlist_id': getattr(self.playlist, 'id', None), + 'added_from': 'sync_page_modal', + 'timestamp': datetime.now().isoformat() + } + + for failed_track_info in self.permanently_failed_tracks: + try: + success = self.wishlist_service.add_failed_track_from_modal( + track_info=failed_track_info, + source_type='playlist', + source_context=source_context + ) + if success: + wishlist_added_count += 1 + except Exception as e: + logger.error(f"Failed to add track to wishlist: {e}") + + if wishlist_added_count > 0: + logger.info(f"Added {wishlist_added_count} failed tracks to wishlist from playlist '{self.playlist.name}'") + + except Exception as e: + logger.error(f"Error adding failed tracks to wishlist: {e}") + # Determine the final message based on success or failure. if self.permanently_failed_tracks: - final_message = f"Completed downloading {self.successful_downloads}/{len(self.missing_tracks)} missing tracks!\n\nYou can now manually correct any failed downloads or close this window." + final_message = f"Completed downloading {self.successful_downloads}/{len(self.missing_tracks)} missing tracks!\n\n" + + if wishlist_added_count > 0: + final_message += f"✨ Added {wishlist_added_count} failed track{'s' if wishlist_added_count != 1 else ''} to wishlist for automatic retry.\n\n" + + final_message += "You can also manually correct failed downloads or check the wishlist on the dashboard." # If there are failures, ensure the modal is visible and bring it to the front. if self.isHidden():