viridian-1.2/locales/msgfmt.sh0000755000175000017500000000020511511672746016246 0ustar charliecharlierm -f locales/it/LC_MESSAGES/viridian.mo mkdir -p locales/it/LC_MESSAGES/ msgfmt -o locales/it/LC_MESSAGES/viridian.mo locales/it.po viridian-1.2/locales/xgettext.sh0000755000175000017500000000020011511672746016620 0ustar charliecharliexgettext -L Python -f locales/files.txt -o locales/messages.pot msgmerge -N locales/it.po locales/messages.pot -o locales/it.po viridian-1.2/locales/files.txt0000644000175000017500000000010311511672746016252 0ustar charliecharlieviridian AmpacheTools/AmpacheGUI.py AmpacheTools/AmpacheSession.py viridian-1.2/locales/it.po0000644000175000017500000004252011511672746015374 0ustar charliecharlie# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2011-01-07 15:12-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: viridian:55 msgid "Application already running.. exiting." msgstr "" #: viridian:59 msgid "Not Running" msgstr "" #: AmpacheTools/AmpacheGUI.py:111 msgid "" "Viridian is still running in the status bar. If you do not want Viridian to " "continue running when the window is closed you can disable it in Preferences." msgstr "" #: AmpacheTools/AmpacheGUI.py:121 msgid "Downloads in progress.." msgstr "Scaricamenti in corso.." #: AmpacheTools/AmpacheGUI.py:121 msgid "There are unfinished downloads, are you sure you want to quit?" msgstr "" #: AmpacheTools/AmpacheGUI.py:207 msgid "_File" msgstr "" #: AmpacheTools/AmpacheGUI.py:210 msgid "Reauthenticate" msgstr "" #: AmpacheTools/AmpacheGUI.py:214 msgid "Open Ampache" msgstr "Apri Ampache" #: AmpacheTools/AmpacheGUI.py:222 msgid "Save Playlist" msgstr "Salva playlist" #: AmpacheTools/AmpacheGUI.py:230 msgid "Load Playlist" msgstr "Apri playlist" #: AmpacheTools/AmpacheGUI.py:238 msgid "Export Playlist..." msgstr "Esporta playlist..." #: AmpacheTools/AmpacheGUI.py:249 msgid "Clear Album Art" msgstr "" #: AmpacheTools/AmpacheGUI.py:253 msgid "Clear Local Cache" msgstr "Svuota cache locale" #: AmpacheTools/AmpacheGUI.py:257 AmpacheTools/AmpacheGUI.py:2546 msgid "Pre-Cache" msgstr "" #: AmpacheTools/AmpacheGUI.py:277 msgid "_Edit" msgstr "_Modifica" #: AmpacheTools/AmpacheGUI.py:280 msgid "Plugins" msgstr "" #: AmpacheTools/AmpacheGUI.py:297 msgid "_View" msgstr "_Visualizza" #: AmpacheTools/AmpacheGUI.py:300 msgid "Show Playlist" msgstr "Mostra playlist" #: AmpacheTools/AmpacheGUI.py:307 msgid "Show Downloads" msgstr "Mostra scaricamenti" #: AmpacheTools/AmpacheGUI.py:316 msgid "View Statusbar" msgstr "Visualizza barra di stato" #: AmpacheTools/AmpacheGUI.py:327 msgid "_Help" msgstr "" #: AmpacheTools/AmpacheGUI.py:396 msgid "Repeat" msgstr "Ripeti" #: AmpacheTools/AmpacheGUI.py:400 msgid "Shuffle" msgstr "Casuale" #: AmpacheTools/AmpacheGUI.py:407 msgid "Volume" msgstr "" #: AmpacheTools/AmpacheGUI.py:556 msgid "Current Playlist" msgstr "Playlist attuale" #: AmpacheTools/AmpacheGUI.py:570 msgid "Clear Playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:576 msgid "Replace Mode" msgstr "Modalità rimpiazza" #: AmpacheTools/AmpacheGUI.py:577 msgid "Add Mode" msgstr "Modalità aggiungi" #: AmpacheTools/AmpacheGUI.py:603 msgid "File" msgstr "File" #: AmpacheTools/AmpacheGUI.py:613 msgid "Progress" msgstr "Progresso" #: AmpacheTools/AmpacheGUI.py:654 msgid "Artists" msgstr "" #: AmpacheTools/AmpacheGUI.py:690 msgid "Albums" msgstr "Album" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:749 msgid "Track" msgstr "Traccia" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:751 msgid "Title" msgstr "Titolo" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:753 msgid "Artist" msgstr "Artista" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:755 msgid "Album" msgstr "Album" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:757 msgid "Time" msgstr "Durata" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:759 msgid "Size" msgstr "Dimensione" #: AmpacheTools/AmpacheGUI.py:782 msgid "Ready" msgstr "Pronto" #: AmpacheTools/AmpacheGUI.py:879 msgid "Attempting to authenticate..." msgstr "Tentativo di autenticazione..." #: AmpacheTools/AmpacheGUI.py:886 msgid "Set Ampache information by going to Edit -> Preferences" msgstr "" #: AmpacheTools/AmpacheGUI.py:888 msgid "" "This looks like the first time you are running Viridian. To get started, go " "to Edit -> Preferences and set your account information." msgstr "" #: AmpacheTools/AmpacheGUI.py:929 msgid "Viridian Settings" msgstr "Impostazioni Viridian" #: AmpacheTools/AmpacheGUI.py:952 msgid "Account Settings" msgstr "Impostazioni account" #: AmpacheTools/AmpacheGUI.py:960 msgid "Ampache URL:" msgstr "URL Ampache:" #: AmpacheTools/AmpacheGUI.py:975 msgid "Example: http://example.com/ampache" msgstr "Esempio: http://esempio.it/ampache" #: AmpacheTools/AmpacheGUI.py:982 msgid "Username:" msgstr "" #: AmpacheTools/AmpacheGUI.py:998 msgid "Password:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1031 msgid "Notifications" msgstr "Notifiche" #: AmpacheTools/AmpacheGUI.py:1040 msgid "Display OSD Notifications" msgstr "" #: AmpacheTools/AmpacheGUI.py:1054 msgid "Alternate Row Colors" msgstr "" #: AmpacheTools/AmpacheGUI.py:1061 msgid "Artists Column" msgstr "Colonna artisti" #: AmpacheTools/AmpacheGUI.py:1069 msgid "Albums Column" msgstr "Colonna album" #: AmpacheTools/AmpacheGUI.py:1077 msgid "Songs Column" msgstr "Colonna brani" #: AmpacheTools/AmpacheGUI.py:1085 msgid "Playlist Column" msgstr "Colonna playlist" #: AmpacheTools/AmpacheGUI.py:1093 msgid "Downloads Column" msgstr "Colonna scaricamenti" #: AmpacheTools/AmpacheGUI.py:1110 msgid "Catalog Cache" msgstr "" #: AmpacheTools/AmpacheGUI.py:1121 msgid "Automatically clear local catalog when Ampache is updated" msgstr "" #: AmpacheTools/AmpacheGUI.py:1139 msgid "Local catalog is up-to-date." msgstr "" #: AmpacheTools/AmpacheGUI.py:1142 msgid "" "Local catalog is older than Ampache catalog! To update the local catalog go " "to File -> Clear Local Cache." msgstr "" #: AmpacheTools/AmpacheGUI.py:1160 msgid "Local Downloads" msgstr "Scaricamenti locali" #: AmpacheTools/AmpacheGUI.py:1168 msgid " Select where downloaded files should go." msgstr "" #: AmpacheTools/AmpacheGUI.py:1201 msgid "Status Tray Icon" msgstr "" #: AmpacheTools/AmpacheGUI.py:1207 msgid "Quit Viridian when window is closed" msgstr "Esci da Viridian quando viene chiusa la finestra" #: AmpacheTools/AmpacheGUI.py:1214 msgid "Standard Tray Icon" msgstr "" #: AmpacheTools/AmpacheGUI.py:1225 msgid "Unified Sound Icon ( Ubuntu 10.10 or higher )" msgstr "" #: AmpacheTools/AmpacheGUI.py:1237 msgid "Disabled" msgstr "" #: AmpacheTools/AmpacheGUI.py:1255 msgid "" "Note: changes to the type of icon will take effect the next time this " "program is opened." msgstr "" #: AmpacheTools/AmpacheGUI.py:1272 msgid "Server Settings" msgstr "Impostazioni server" #: AmpacheTools/AmpacheGUI.py:1281 msgid "XML RPC Server: " msgstr "" #: AmpacheTools/AmpacheGUI.py:1288 AmpacheTools/AmpacheGUI.py:2646 #, python-format msgid "Running. (port %d)" msgstr "In esecuzione. (porta %d)" #: AmpacheTools/AmpacheGUI.py:1291 AmpacheTools/AmpacheGUI.py:2649 msgid "Not Running." msgstr "Non in esecuzione." #: AmpacheTools/AmpacheGUI.py:1303 msgid "Start" msgstr "Avvia" #: AmpacheTools/AmpacheGUI.py:1308 msgid "Stop" msgstr "Termina" #: AmpacheTools/AmpacheGUI.py:1313 msgid "Restart" msgstr "Riavvia" #: AmpacheTools/AmpacheGUI.py:1318 msgid "Port: " msgstr "Porta: " #: AmpacheTools/AmpacheGUI.py:1330 msgid "Start XML RPC server when Viridan starts" msgstr "" #: AmpacheTools/AmpacheGUI.py:1349 msgid "System" msgstr "Sistema" #: AmpacheTools/AmpacheGUI.py:1359 msgid "" "To delete all personal information (including your username, password, album-" "art, cached information, etc.) press this button. NOTE: This will delete all " "personal settings stored on this computer and Viridian will close itself. " "When you reopen, it will be as though it is the first time you are running " "Viridian." msgstr "" #: AmpacheTools/AmpacheGUI.py:1370 msgid "Reset Everything" msgstr "" #: AmpacheTools/AmpacheGUI.py:1379 msgid "Account" msgstr "" #: AmpacheTools/AmpacheGUI.py:1380 msgid "Display" msgstr "Aspetto" #: AmpacheTools/AmpacheGUI.py:1381 msgid "Catalog" msgstr "" #: AmpacheTools/AmpacheGUI.py:1382 msgid "Downloads" msgstr "Scaricamenti" #: AmpacheTools/AmpacheGUI.py:1383 msgid "Tray Icon" msgstr "" #: AmpacheTools/AmpacheGUI.py:1384 msgid "Server" msgstr "Server" #: AmpacheTools/AmpacheGUI.py:1385 msgid "System" msgstr "Sistema" #: AmpacheTools/AmpacheGUI.py:1420 msgid "Viridian Help" msgstr "Guida di Viridian" #: AmpacheTools/AmpacheGUI.py:1432 msgid "Viridian Help" msgstr "" #: AmpacheTools/AmpacheGUI.py:1436 msgid "Home Page:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1443 msgid "Launchpad:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1450 msgid "FAQ:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1457 msgid "Bugs:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1464 msgid "Questions:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1517 AmpacheTools/AmpacheGUI.py:1518 #: AmpacheTools/AmpacheGUI.py:1526 AmpacheTools/AmpacheGUI.py:1649 msgid "Name" msgstr "Nome" #: AmpacheTools/AmpacheGUI.py:1517 AmpacheTools/AmpacheGUI.py:1528 msgid "Songs" msgstr "Brani" #: AmpacheTools/AmpacheGUI.py:1517 AmpacheTools/AmpacheGUI.py:1530 msgid "Owner" msgstr "Proprietario" #: AmpacheTools/AmpacheGUI.py:1517 AmpacheTools/AmpacheGUI.py:1532 msgid "Type" msgstr "Tipo" #: AmpacheTools/AmpacheGUI.py:1546 msgid "Playlist Name: " msgstr "Nome playlist" #: AmpacheTools/AmpacheGUI.py:1563 msgid "Export as M3U..." msgstr "Esporta come M3U..." #: AmpacheTools/AmpacheGUI.py:1626 msgid "Viridian Plugins" msgstr "" #: AmpacheTools/AmpacheGUI.py:1632 msgid "Select a plugin." msgstr "" #: AmpacheTools/AmpacheGUI.py:1645 msgid "Enabled" msgstr "" #: AmpacheTools/AmpacheGUI.py:1653 msgid "Description" msgstr "" #: AmpacheTools/AmpacheGUI.py:1674 #, python-format msgid "" "Error: plugin '%s' could not be loaded because it is missing a title, " "description, and author instance variable" msgstr "" #: AmpacheTools/AmpacheGUI.py:1712 msgid "Show Window" msgstr "Mostra finestra" #: AmpacheTools/AmpacheGUI.py:1719 msgid "- Now Playing -" msgstr "- In esecuzione -" #: AmpacheTools/AmpacheGUI.py:1721 msgid "- Now Playing (paused) -" msgstr "- In esecuzione (pausa) -" #: AmpacheTools/AmpacheGUI.py:1897 msgid "Authenticating..." msgstr "" #: AmpacheTools/AmpacheGUI.py:1949 msgid "Pulling Artists..." msgstr "" #: AmpacheTools/AmpacheGUI.py:1953 #, python-format msgid "All Artists (%d)" msgstr "" #: AmpacheTools/AmpacheGUI.py:1959 msgid "Pulling Albums..." msgstr "" #: AmpacheTools/AmpacheGUI.py:1962 msgid "Ready." msgstr "Pronto." #: AmpacheTools/AmpacheGUI.py:1967 msgid "" "Unknown error, possibly an incorrect URL specified, or the server is not " "responding." msgstr "" #: AmpacheTools/AmpacheGUI.py:1968 msgid "Authentication Failed." msgstr "Autenticazione fallita." #: AmpacheTools/AmpacheGUI.py:1969 msgid "" "Error Authenticating\n" "\n" msgstr "" #: AmpacheTools/AmpacheGUI.py:2007 #, python-format msgid "All Albums (%d)" msgstr "Tutti gli album (%d)" #: AmpacheTools/AmpacheGUI.py:2054 msgid "Fetching Album id: " msgstr "" #: AmpacheTools/AmpacheGUI.py:2138 AmpacheTools/AmpacheGUI.py:2154 msgid "Remove From Playlist" msgstr "Rimuovi dalla playlist" #: AmpacheTools/AmpacheGUI.py:2142 AmpacheTools/AmpacheGUI.py:2223 msgid "Download Songs" msgstr "Scarica brani" #: AmpacheTools/AmpacheGUI.py:2158 AmpacheTools/AmpacheGUI.py:2239 msgid "Download Song" msgstr "Scarica brano" #: AmpacheTools/AmpacheGUI.py:2161 AmpacheTools/AmpacheGUI.py:2242 msgid "Copy URL to Clipboard" msgstr "" #: AmpacheTools/AmpacheGUI.py:2177 msgid "Open Song" msgstr "Apri brano" #: AmpacheTools/AmpacheGUI.py:2180 msgid "Open Containing Folder" msgstr "" #: AmpacheTools/AmpacheGUI.py:2196 msgid "Add Album to Playlist" msgstr "Aggiungi album alla playlist" #: AmpacheTools/AmpacheGUI.py:2200 msgid "Download Album" msgstr "Scarica album" #: AmpacheTools/AmpacheGUI.py:2219 msgid "Add Songs to Playlist" msgstr "Aggiungi brani alla playlist" #: AmpacheTools/AmpacheGUI.py:2235 msgid "Add Song to Playlist" msgstr "Aggiungi brano alla playlist" #: AmpacheTools/AmpacheGUI.py:2317 msgid "Saved Credentials" msgstr "Credenziali salvate" #: AmpacheTools/AmpacheGUI.py:2318 msgid "Credentials Saved" msgstr "Credenziali salvate." #: AmpacheTools/AmpacheGUI.py:2322 msgid "Couldn't save credentials!" msgstr "Impossibile salvare credenziali!" #: AmpacheTools/AmpacheGUI.py:2323 msgid "[Error] Couldn't save credentials!" msgstr "[Errore] Impossibile salvare credenziali" #: AmpacheTools/AmpacheGUI.py:2334 msgid "Choose Folder..." msgstr "Seleziona cartella..." #: AmpacheTools/AmpacheGUI.py:2392 AmpacheTools/AmpacheGUI.py:2438 #: AmpacheTools/AmpacheGUI.py:2439 msgid "Cannot save empty playlist." msgstr "" #: AmpacheTools/AmpacheGUI.py:2393 msgid "Cannot save empty playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:2430 #, python-format msgid "Problem loading playlist. Playlist ID = %d" msgstr "" #: AmpacheTools/AmpacheGUI.py:2442 msgid "Invalid Name." msgstr "" #: AmpacheTools/AmpacheGUI.py:2445 msgid "Overwrite Playlist?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2445 #, python-format msgid "A playlist by the name '%s' already exists, overwrite?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2456 msgid "Only exporting of local playlists is supported." msgstr "" #: AmpacheTools/AmpacheGUI.py:2461 msgid "Cannot export empty playlist." msgstr "" #: AmpacheTools/AmpacheGUI.py:2470 msgid "File already exists." msgstr "" #: AmpacheTools/AmpacheGUI.py:2481 #, python-format msgid "Playlist %s written to %s." msgstr "" #: AmpacheTools/AmpacheGUI.py:2494 msgid "Delete Playlist?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2494 #, python-format msgid "Are you sure you want to delete the playlist '%s'?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2500 msgid "Cannot delete playlists that are on the Ampache server from Viridian." msgstr "" #: AmpacheTools/AmpacheGUI.py:2525 msgid "Album Art Cleared" msgstr "" #: AmpacheTools/AmpacheGUI.py:2529 msgid "Reset Viridian" msgstr "" #: AmpacheTools/AmpacheGUI.py:2529 msgid "" "Are you sure you want to delete all personal information stored with " "Viridian?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2537 msgid "Not Authenticated" msgstr "Non autenticato" #: AmpacheTools/AmpacheGUI.py:2542 msgid "Pre-Cache already in progress." msgstr "" #: AmpacheTools/AmpacheGUI.py:2546 msgid "" "This will cache all of the artist, album, and song information (not the " "songs themselves) locally to make Viridian respond faster.\n" "\n" "This process can take a long time depending on the size of your catalog. " "Proceed?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2566 #, python-format msgid "Pulling all albums from artists: %d/%d" msgstr "" #: AmpacheTools/AmpacheGUI.py:2568 msgid "Finished pulling albums" msgstr "" #: AmpacheTools/AmpacheGUI.py:2579 #, python-format msgid "Pulling all songs from albums: %d/%d" msgstr "" #: AmpacheTools/AmpacheGUI.py:2585 msgid "Finished Pre Cache -- Time Taken: " msgstr "" #: AmpacheTools/AmpacheGUI.py:2589 msgid "Error with pre-cache!" msgstr "" #: AmpacheTools/AmpacheGUI.py:2591 msgid "" "Error with pre-cache!\n" "\n" msgstr "" #: AmpacheTools/AmpacheGUI.py:2613 msgid "Open Image" msgstr "Apri immagine" #: AmpacheTools/AmpacheGUI.py:2616 msgid "Refresh Album Art" msgstr "Aggiorna copertine" #: AmpacheTools/AmpacheGUI.py:2702 msgid "Viridian is a front-end for an Ampache Server (see http://ampache.org)" msgstr "" "Viridian è un'interfaccia per un server Ampache (vedi http://ampache.org)" #: AmpacheTools/AmpacheGUI.py:2724 msgid "Ampache Catalog Updated" msgstr "" #: AmpacheTools/AmpacheGUI.py:2724 msgid "" "The Ampache catalog on the server is newer than the locally cached catalog " "on this computer.\n" "Would you like to update the local catalog by clearing the local cache?\n" "\n" "(You can also do this at anytime by going to File -> Clear Local Cache)." msgstr "" #: AmpacheTools/AmpacheGUI.py:2810 msgid "Now Playing" msgstr "Ora in esecuzione" #: AmpacheTools/AmpacheGUI.py:2842 msgid "An error has occured." msgstr "Si è verificato un errore." #: AmpacheTools/AmpacheGUI.py:2843 #, python-format msgid "" "GStreamer has encountered an error, this is most likely caused by:\n" "- gstreamer-plugins not being installed.\n" "- Ampache not transcoding the file correctly.\n" "- A lost or dropped connection to the server.\n" "\t\t\n" "Message from GStreamer:\n" "%s" msgstr "" #: AmpacheTools/AmpacheGUI.py:2909 msgid "The file/URL specified is invalid." msgstr "" #: AmpacheTools/AmpacheGUI.py:3006 msgid "Loading Playlist..." msgstr "Caricamento playlist..." #: AmpacheTools/AmpacheGUI.py:3010 #, python-format msgid "Querying for song %d/%d in playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:3017 msgid "Playlist loaded" msgstr "Playlist caricata" #: AmpacheTools/AmpacheGUI.py:3122 AmpacheTools/AmpacheGUI.py:3135 #: AmpacheTools/AmpacheGUI.py:3148 #, python-format msgid "" "The folder %s does not exist. You can change the folder in Preferences." msgstr "" #: AmpacheTools/AmpacheGUI.py:3167 msgid "Download Complete" msgstr "Scaricamento completato" #: AmpacheTools/AmpacheGUI.py:3256 msgid "Re-Fetching album art..." msgstr "" #: AmpacheTools/AmpacheGUI.py:3268 msgid "Re-Fetching album art... Failed!" msgstr "" #: AmpacheTools/AmpacheGUI.py:3276 msgid "Re-Fetching album art... Success!" msgstr "" #: AmpacheTools/AmpacheGUI.py:3288 #, python-format msgid "Error with album -- Check Ampache -- Album ID = %d" msgstr "" viridian-1.2/locales/messages.pot0000644000175000017500000003761111511672746016760 0ustar charliecharlie# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2011-01-07 15:12-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: viridian:55 msgid "Application already running.. exiting." msgstr "" #: viridian:59 msgid "Not Running" msgstr "" #: AmpacheTools/AmpacheGUI.py:111 msgid "" "Viridian is still running in the status bar. If you do not want Viridian to " "continue running when the window is closed you can disable it in Preferences." msgstr "" #: AmpacheTools/AmpacheGUI.py:121 msgid "Downloads in progress.." msgstr "" #: AmpacheTools/AmpacheGUI.py:121 msgid "There are unfinished downloads, are you sure you want to quit?" msgstr "" #: AmpacheTools/AmpacheGUI.py:207 msgid "_File" msgstr "" #: AmpacheTools/AmpacheGUI.py:210 msgid "Reauthenticate" msgstr "" #: AmpacheTools/AmpacheGUI.py:214 msgid "Open Ampache" msgstr "" #: AmpacheTools/AmpacheGUI.py:222 msgid "Save Playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:230 msgid "Load Playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:238 msgid "Export Playlist..." msgstr "" #: AmpacheTools/AmpacheGUI.py:249 msgid "Clear Album Art" msgstr "" #: AmpacheTools/AmpacheGUI.py:253 msgid "Clear Local Cache" msgstr "" #: AmpacheTools/AmpacheGUI.py:257 AmpacheTools/AmpacheGUI.py:2546 msgid "Pre-Cache" msgstr "" #: AmpacheTools/AmpacheGUI.py:277 msgid "_Edit" msgstr "" #: AmpacheTools/AmpacheGUI.py:280 msgid "Plugins" msgstr "" #: AmpacheTools/AmpacheGUI.py:297 msgid "_View" msgstr "" #: AmpacheTools/AmpacheGUI.py:300 msgid "Show Playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:307 msgid "Show Downloads" msgstr "" #: AmpacheTools/AmpacheGUI.py:316 msgid "View Statusbar" msgstr "" #: AmpacheTools/AmpacheGUI.py:327 msgid "_Help" msgstr "" #: AmpacheTools/AmpacheGUI.py:396 msgid "Repeat" msgstr "" #: AmpacheTools/AmpacheGUI.py:400 msgid "Shuffle" msgstr "" #: AmpacheTools/AmpacheGUI.py:407 msgid "Volume" msgstr "" #: AmpacheTools/AmpacheGUI.py:556 msgid "Current Playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:570 msgid "Clear Playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:576 msgid "Replace Mode" msgstr "" #: AmpacheTools/AmpacheGUI.py:577 msgid "Add Mode" msgstr "" #: AmpacheTools/AmpacheGUI.py:603 msgid "File" msgstr "" #: AmpacheTools/AmpacheGUI.py:613 msgid "Progress" msgstr "" #: AmpacheTools/AmpacheGUI.py:654 msgid "Artists" msgstr "" #: AmpacheTools/AmpacheGUI.py:690 msgid "Albums" msgstr "" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:749 msgid "Track" msgstr "" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:751 msgid "Title" msgstr "" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:753 msgid "Artist" msgstr "" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:755 msgid "Album" msgstr "" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:757 msgid "Time" msgstr "" #: AmpacheTools/AmpacheGUI.py:744 AmpacheTools/AmpacheGUI.py:759 msgid "Size" msgstr "" #: AmpacheTools/AmpacheGUI.py:782 msgid "Ready" msgstr "" #: AmpacheTools/AmpacheGUI.py:879 msgid "Attempting to authenticate..." msgstr "" #: AmpacheTools/AmpacheGUI.py:886 msgid "Set Ampache information by going to Edit -> Preferences" msgstr "" #: AmpacheTools/AmpacheGUI.py:888 msgid "" "This looks like the first time you are running Viridian. To get started, go " "to Edit -> Preferences and set your account information." msgstr "" #: AmpacheTools/AmpacheGUI.py:929 msgid "Viridian Settings" msgstr "" #: AmpacheTools/AmpacheGUI.py:952 msgid "Account Settings" msgstr "" #: AmpacheTools/AmpacheGUI.py:960 msgid "Ampache URL:" msgstr "" #: AmpacheTools/AmpacheGUI.py:975 msgid "Example: http://example.com/ampache" msgstr "" #: AmpacheTools/AmpacheGUI.py:982 msgid "Username:" msgstr "" #: AmpacheTools/AmpacheGUI.py:998 msgid "Password:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1031 msgid "Notifications" msgstr "" #: AmpacheTools/AmpacheGUI.py:1040 msgid "Display OSD Notifications" msgstr "" #: AmpacheTools/AmpacheGUI.py:1054 msgid "Alternate Row Colors" msgstr "" #: AmpacheTools/AmpacheGUI.py:1061 msgid "Artists Column" msgstr "" #: AmpacheTools/AmpacheGUI.py:1069 msgid "Albums Column" msgstr "" #: AmpacheTools/AmpacheGUI.py:1077 msgid "Songs Column" msgstr "" #: AmpacheTools/AmpacheGUI.py:1085 msgid "Playlist Column" msgstr "" #: AmpacheTools/AmpacheGUI.py:1093 msgid "Downloads Column" msgstr "" #: AmpacheTools/AmpacheGUI.py:1110 msgid "Catalog Cache" msgstr "" #: AmpacheTools/AmpacheGUI.py:1121 msgid "Automatically clear local catalog when Ampache is updated" msgstr "" #: AmpacheTools/AmpacheGUI.py:1139 msgid "Local catalog is up-to-date." msgstr "" #: AmpacheTools/AmpacheGUI.py:1142 msgid "" "Local catalog is older than Ampache catalog! To update the local catalog go " "to File -> Clear Local Cache." msgstr "" #: AmpacheTools/AmpacheGUI.py:1160 msgid "Local Downloads" msgstr "" #: AmpacheTools/AmpacheGUI.py:1168 msgid " Select where downloaded files should go." msgstr "" #: AmpacheTools/AmpacheGUI.py:1201 msgid "Status Tray Icon" msgstr "" #: AmpacheTools/AmpacheGUI.py:1207 msgid "Quit Viridian when window is closed" msgstr "" #: AmpacheTools/AmpacheGUI.py:1214 msgid "Standard Tray Icon" msgstr "" #: AmpacheTools/AmpacheGUI.py:1225 msgid "Unified Sound Icon ( Ubuntu 10.10 or higher )" msgstr "" #: AmpacheTools/AmpacheGUI.py:1237 msgid "Disabled" msgstr "" #: AmpacheTools/AmpacheGUI.py:1255 msgid "" "Note: changes to the type of icon will take effect the next time this " "program is opened." msgstr "" #: AmpacheTools/AmpacheGUI.py:1272 msgid "Server Settings" msgstr "" #: AmpacheTools/AmpacheGUI.py:1281 msgid "XML RPC Server: " msgstr "" #: AmpacheTools/AmpacheGUI.py:1288 AmpacheTools/AmpacheGUI.py:2646 #, python-format msgid "Running. (port %d)" msgstr "" #: AmpacheTools/AmpacheGUI.py:1291 AmpacheTools/AmpacheGUI.py:2649 msgid "Not Running." msgstr "" #: AmpacheTools/AmpacheGUI.py:1303 msgid "Start" msgstr "" #: AmpacheTools/AmpacheGUI.py:1308 msgid "Stop" msgstr "" #: AmpacheTools/AmpacheGUI.py:1313 msgid "Restart" msgstr "" #: AmpacheTools/AmpacheGUI.py:1318 msgid "Port: " msgstr "" #: AmpacheTools/AmpacheGUI.py:1330 msgid "Start XML RPC server when Viridan starts" msgstr "" #: AmpacheTools/AmpacheGUI.py:1349 msgid "System" msgstr "" #: AmpacheTools/AmpacheGUI.py:1359 msgid "" "To delete all personal information (including your username, password, album-" "art, cached information, etc.) press this button. NOTE: This will delete all " "personal settings stored on this computer and Viridian will close itself. " "When you reopen, it will be as though it is the first time you are running " "Viridian." msgstr "" #: AmpacheTools/AmpacheGUI.py:1370 msgid "Reset Everything" msgstr "" #: AmpacheTools/AmpacheGUI.py:1379 msgid "Account" msgstr "" #: AmpacheTools/AmpacheGUI.py:1380 msgid "Display" msgstr "" #: AmpacheTools/AmpacheGUI.py:1381 msgid "Catalog" msgstr "" #: AmpacheTools/AmpacheGUI.py:1382 msgid "Downloads" msgstr "" #: AmpacheTools/AmpacheGUI.py:1383 msgid "Tray Icon" msgstr "" #: AmpacheTools/AmpacheGUI.py:1384 msgid "Server" msgstr "" #: AmpacheTools/AmpacheGUI.py:1385 msgid "System" msgstr "" #: AmpacheTools/AmpacheGUI.py:1420 msgid "Viridian Help" msgstr "" #: AmpacheTools/AmpacheGUI.py:1432 msgid "Viridian Help" msgstr "" #: AmpacheTools/AmpacheGUI.py:1436 msgid "Home Page:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1443 msgid "Launchpad:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1450 msgid "FAQ:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1457 msgid "Bugs:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1464 msgid "Questions:" msgstr "" #: AmpacheTools/AmpacheGUI.py:1517 AmpacheTools/AmpacheGUI.py:1518 #: AmpacheTools/AmpacheGUI.py:1526 AmpacheTools/AmpacheGUI.py:1649 msgid "Name" msgstr "" #: AmpacheTools/AmpacheGUI.py:1517 AmpacheTools/AmpacheGUI.py:1528 msgid "Songs" msgstr "" #: AmpacheTools/AmpacheGUI.py:1517 AmpacheTools/AmpacheGUI.py:1530 msgid "Owner" msgstr "" #: AmpacheTools/AmpacheGUI.py:1517 AmpacheTools/AmpacheGUI.py:1532 msgid "Type" msgstr "" #: AmpacheTools/AmpacheGUI.py:1546 msgid "Playlist Name: " msgstr "" #: AmpacheTools/AmpacheGUI.py:1563 msgid "Export as M3U..." msgstr "" #: AmpacheTools/AmpacheGUI.py:1626 msgid "Viridian Plugins" msgstr "" #: AmpacheTools/AmpacheGUI.py:1632 msgid "Select a plugin." msgstr "" #: AmpacheTools/AmpacheGUI.py:1645 msgid "Enabled" msgstr "" #: AmpacheTools/AmpacheGUI.py:1653 msgid "Description" msgstr "" #: AmpacheTools/AmpacheGUI.py:1674 #, python-format msgid "" "Error: plugin '%s' could not be loaded because it is missing a title, " "description, and author instance variable" msgstr "" #: AmpacheTools/AmpacheGUI.py:1712 msgid "Show Window" msgstr "" #: AmpacheTools/AmpacheGUI.py:1719 msgid "- Now Playing -" msgstr "" #: AmpacheTools/AmpacheGUI.py:1721 msgid "- Now Playing (paused) -" msgstr "" #: AmpacheTools/AmpacheGUI.py:1897 msgid "Authenticating..." msgstr "" #: AmpacheTools/AmpacheGUI.py:1949 msgid "Pulling Artists..." msgstr "" #: AmpacheTools/AmpacheGUI.py:1953 #, python-format msgid "All Artists (%d)" msgstr "" #: AmpacheTools/AmpacheGUI.py:1959 msgid "Pulling Albums..." msgstr "" #: AmpacheTools/AmpacheGUI.py:1962 msgid "Ready." msgstr "" #: AmpacheTools/AmpacheGUI.py:1967 msgid "" "Unknown error, possibly an incorrect URL specified, or the server is not " "responding." msgstr "" #: AmpacheTools/AmpacheGUI.py:1968 msgid "Authentication Failed." msgstr "" #: AmpacheTools/AmpacheGUI.py:1969 msgid "" "Error Authenticating\n" "\n" msgstr "" #: AmpacheTools/AmpacheGUI.py:2007 #, python-format msgid "All Albums (%d)" msgstr "" #: AmpacheTools/AmpacheGUI.py:2054 msgid "Fetching Album id: " msgstr "" #: AmpacheTools/AmpacheGUI.py:2138 AmpacheTools/AmpacheGUI.py:2154 msgid "Remove From Playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:2142 AmpacheTools/AmpacheGUI.py:2223 msgid "Download Songs" msgstr "" #: AmpacheTools/AmpacheGUI.py:2158 AmpacheTools/AmpacheGUI.py:2239 msgid "Download Song" msgstr "" #: AmpacheTools/AmpacheGUI.py:2161 AmpacheTools/AmpacheGUI.py:2242 msgid "Copy URL to Clipboard" msgstr "" #: AmpacheTools/AmpacheGUI.py:2177 msgid "Open Song" msgstr "" #: AmpacheTools/AmpacheGUI.py:2180 msgid "Open Containing Folder" msgstr "" #: AmpacheTools/AmpacheGUI.py:2196 msgid "Add Album to Playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:2200 msgid "Download Album" msgstr "" #: AmpacheTools/AmpacheGUI.py:2219 msgid "Add Songs to Playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:2235 msgid "Add Song to Playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:2317 msgid "Saved Credentials" msgstr "" #: AmpacheTools/AmpacheGUI.py:2318 msgid "Credentials Saved" msgstr "" #: AmpacheTools/AmpacheGUI.py:2322 msgid "Couldn't save credentials!" msgstr "" #: AmpacheTools/AmpacheGUI.py:2323 msgid "[Error] Couldn't save credentials!" msgstr "" #: AmpacheTools/AmpacheGUI.py:2334 msgid "Choose Folder..." msgstr "" #: AmpacheTools/AmpacheGUI.py:2392 AmpacheTools/AmpacheGUI.py:2438 #: AmpacheTools/AmpacheGUI.py:2439 msgid "Cannot save empty playlist." msgstr "" #: AmpacheTools/AmpacheGUI.py:2393 msgid "Cannot save empty playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:2430 #, python-format msgid "Problem loading playlist. Playlist ID = %d" msgstr "" #: AmpacheTools/AmpacheGUI.py:2442 msgid "Invalid Name." msgstr "" #: AmpacheTools/AmpacheGUI.py:2445 msgid "Overwrite Playlist?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2445 #, python-format msgid "A playlist by the name '%s' already exists, overwrite?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2456 msgid "Only exporting of local playlists is supported." msgstr "" #: AmpacheTools/AmpacheGUI.py:2461 msgid "Cannot export empty playlist." msgstr "" #: AmpacheTools/AmpacheGUI.py:2470 msgid "File already exists." msgstr "" #: AmpacheTools/AmpacheGUI.py:2481 #, python-format msgid "Playlist %s written to %s." msgstr "" #: AmpacheTools/AmpacheGUI.py:2494 msgid "Delete Playlist?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2494 #, python-format msgid "Are you sure you want to delete the playlist '%s'?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2500 msgid "Cannot delete playlists that are on the Ampache server from Viridian." msgstr "" #: AmpacheTools/AmpacheGUI.py:2525 msgid "Album Art Cleared" msgstr "" #: AmpacheTools/AmpacheGUI.py:2529 msgid "Reset Viridian" msgstr "" #: AmpacheTools/AmpacheGUI.py:2529 msgid "" "Are you sure you want to delete all personal information stored with " "Viridian?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2537 msgid "Not Authenticated" msgstr "" #: AmpacheTools/AmpacheGUI.py:2542 msgid "Pre-Cache already in progress." msgstr "" #: AmpacheTools/AmpacheGUI.py:2546 msgid "" "This will cache all of the artist, album, and song information (not the " "songs themselves) locally to make Viridian respond faster.\n" "\n" "This process can take a long time depending on the size of your catalog. " "Proceed?" msgstr "" #: AmpacheTools/AmpacheGUI.py:2566 #, python-format msgid "Pulling all albums from artists: %d/%d" msgstr "" #: AmpacheTools/AmpacheGUI.py:2568 msgid "Finished pulling albums" msgstr "" #: AmpacheTools/AmpacheGUI.py:2579 #, python-format msgid "Pulling all songs from albums: %d/%d" msgstr "" #: AmpacheTools/AmpacheGUI.py:2585 msgid "Finished Pre Cache -- Time Taken: " msgstr "" #: AmpacheTools/AmpacheGUI.py:2589 msgid "Error with pre-cache!" msgstr "" #: AmpacheTools/AmpacheGUI.py:2591 msgid "" "Error with pre-cache!\n" "\n" msgstr "" #: AmpacheTools/AmpacheGUI.py:2613 msgid "Open Image" msgstr "" #: AmpacheTools/AmpacheGUI.py:2616 msgid "Refresh Album Art" msgstr "" #: AmpacheTools/AmpacheGUI.py:2702 msgid "Viridian is a front-end for an Ampache Server (see http://ampache.org)" msgstr "" #: AmpacheTools/AmpacheGUI.py:2724 msgid "Ampache Catalog Updated" msgstr "" #: AmpacheTools/AmpacheGUI.py:2724 msgid "" "The Ampache catalog on the server is newer than the locally cached catalog " "on this computer.\n" "Would you like to update the local catalog by clearing the local cache?\n" "\n" "(You can also do this at anytime by going to File -> Clear Local Cache)." msgstr "" #: AmpacheTools/AmpacheGUI.py:2810 msgid "Now Playing" msgstr "" #: AmpacheTools/AmpacheGUI.py:2842 msgid "An error has occured." msgstr "" #: AmpacheTools/AmpacheGUI.py:2843 #, python-format msgid "" "GStreamer has encountered an error, this is most likely caused by:\n" "- gstreamer-plugins not being installed.\n" "- Ampache not transcoding the file correctly.\n" "- A lost or dropped connection to the server.\n" "\t\t\n" "Message from GStreamer:\n" "%s" msgstr "" #: AmpacheTools/AmpacheGUI.py:2909 msgid "The file/URL specified is invalid." msgstr "" #: AmpacheTools/AmpacheGUI.py:3006 msgid "Loading Playlist..." msgstr "" #: AmpacheTools/AmpacheGUI.py:3010 #, python-format msgid "Querying for song %d/%d in playlist" msgstr "" #: AmpacheTools/AmpacheGUI.py:3017 msgid "Playlist loaded" msgstr "" #: AmpacheTools/AmpacheGUI.py:3122 AmpacheTools/AmpacheGUI.py:3135 #: AmpacheTools/AmpacheGUI.py:3148 #, python-format msgid "" "The folder %s does not exist. You can change the folder in Preferences." msgstr "" #: AmpacheTools/AmpacheGUI.py:3167 msgid "Download Complete" msgstr "" #: AmpacheTools/AmpacheGUI.py:3256 msgid "Re-Fetching album art..." msgstr "" #: AmpacheTools/AmpacheGUI.py:3268 msgid "Re-Fetching album art... Failed!" msgstr "" #: AmpacheTools/AmpacheGUI.py:3276 msgid "Re-Fetching album art... Success!" msgstr "" #: AmpacheTools/AmpacheGUI.py:3288 #, python-format msgid "Error with album -- Check Ampache -- Album ID = %d" msgstr "" viridian-1.2/locales/it/LC_MESSAGES/viridian.mo0000644000175000017500000001204511511672746020762 0ustar charliecharlieW  $C2v    2 I Z l         # 6 G L Z n s       # * 0 7 I ^ e r z              % 4 B FT "   @  %7SoB%8Urx ~  .Odu }    ! &4LQat      0-4<Ofm  (.6>ELTYsJ(  TK7&L!S'O( IH)B02 A?DVW1+JC.9PU: N" E=3,Q>*G <# /%$F@46;58-RM- Now Playing (paused) -- Now Playing -Account SettingsAll Albums (%d)Local DownloadsNotificationsServer SettingsSystemExample: http://example.com/ampacheAdd Album to PlaylistAdd ModeAdd Song to PlaylistAdd Songs to PlaylistAlbumAlbumsAlbums ColumnAmpache URL:An error has occured.ArtistArtists ColumnAttempting to authenticate...Authentication Failed.Choose Folder...Clear Local CacheCouldn't save credentials!Credentials SavedCurrent PlaylistDisplayDownload AlbumDownload CompleteDownload SongDownload SongsDownloadsDownloads ColumnDownloads in progress..Export Playlist...Export as M3U...FileLoad PlaylistLoading Playlist...NameNot AuthenticatedNot Running.Now PlayingOpen AmpacheOpen ImageOpen SongOwnerPlaylist ColumnPlaylist Name: Playlist loadedPort: ProgressQuit Viridian when window is closedReadyReady.Refresh Album ArtRemove From PlaylistRepeatReplace ModeRestartRunning. (port %d)Save PlaylistSaved CredentialsServerShow DownloadsShow PlaylistShow WindowShuffleSizeSongsSongs ColumnStartStopSystemTimeTitleTrackTypeView StatusbarViridian HelpViridian SettingsViridian is a front-end for an Ampache Server (see http://ampache.org)[Error] Couldn't save credentials!_Edit_ViewProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2010-12-27 21:14-0500 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - In esecuzione (pausa) -- In esecuzione -Impostazioni accountTutti gli album (%d)Scaricamenti localiNotificheImpostazioni serverSistemaEsempio: http://esempio.it/ampacheAggiungi album alla playlistModalità aggiungiAggiungi brano alla playlistAggiungi brani alla playlistAlbumAlbumColonna albumURL Ampache:Si è verificato un errore.ArtistaColonna artistiTentativo di autenticazione...Autenticazione fallita.Seleziona cartella...Svuota cache localeImpossibile salvare credenziali!Credenziali salvate.Playlist attualeAspettoScarica albumScaricamento completatoScarica branoScarica braniScaricamentiColonna scaricamentiScaricamenti in corso..Esporta playlist...Esporta come M3U...FileApri playlistCaricamento playlist...NomeNon autenticatoNon in esecuzione.Ora in esecuzioneApri AmpacheApri immagineApri branoProprietarioColonna playlistNome playlistPlaylist caricataPorta: ProgressoEsci da Viridian quando viene chiusa la finestraProntoPronto.Aggiorna copertineRimuovi dalla playlistRipetiModalità rimpiazzaRiavviaIn esecuzione. (porta %d)Salva playlistCredenziali salvateServerMostra scaricamentiMostra playlistMostra finestraCasualeDimensioneBraniColonna braniAvviaTerminaSistemaDurataTitoloTracciaTipoVisualizza barra di statoGuida di ViridianImpostazioni ViridianViridian è un'interfaccia per un server Ampache (vedi http://ampache.org)[Errore] Impossibile salvare credenziali_Modifica_Visualizzaviridian-1.2/setup.py0000755000175000017500000000205211511672746014507 0ustar charliecharlie#!/usr/bin/env python ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE from distutils.core import setup setup(name='Viridian', version='1.1.1', description='Viridian Media Player', author='Dave Eddy', author_email='dave@daveeddy.com', url='http://viridian.daveeddy.com', scripts=['viridian', 'viridian-cli'], packages=['AmpacheTools'], package_data={'AmpacheTools' : ['images/*', 'doc/*', 'locales/*', 'plugins/*']} ) viridian-1.2/ViridianApp.svg0000644000175000017500000042525311511672746015735 0ustar charliecharlie viridian-1.2/viridian-cli0000755000175000017500000001034611511672746015277 0ustar charliecharlie#!/usr/bin/env python # -*- coding: utf-8 -*- ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE __program__ = "Viridian-cli" __author__ = "David Eddy" __license__ = "GPLv3" __version__ = "1.1" __maintainer__ = "David Eddy" __email__ = "dave@daveeddy.com" __status__ = "Release" import AmpacheTools from AmpacheTools import AmpacheSession, DatabaseSession, AudioEngine import os from getpass import getpass VIRIDIAN_DIR = os.path.expanduser("~") + os.sep + '.viridian' def get_valid_digit(prompt = "Enter data: "): valid = False while not valid: _id = raw_input("\n\n%s" % (prompt)) if _id.isdigit(): valid = True else: print "Error! Invalid Album ID" return _id def print_now_playing(): song_id = audio_engine.get_current_song_id() song_dict = ampache_conn.get_song_info(song_id) print " - Now Playing - " print song_dict['song_title'] print "By: %s" % song_dict['artist_name'] print "On: %s" % song_dict['album_name'] def next_track(): audio_engine.next_track() print_now_playing() def prev_track(): audio_engine.prev_track() print_now_playing() if __name__ == "__main__": is_first_time = True if os.path.exists(VIRIDIAN_DIR): is_first_time = False db_session = DatabaseSession(VIRIDIAN_DIR + os.sep + 'viridian.sqlite') ampache_conn = AmpacheSession() audio_engine = AudioEngine(ampache_conn) audio_engine.set_repeat_songs(True) ready = False if not is_first_time: username = db_session.variable_get('credentials_username') password = db_session.variable_get('credentials_password') url = db_session.variable_get('credentials_url') ampache_conn.set_credentials(username, password, url) if ampache_conn.has_credentials(): resp = raw_input("Username '%s' found for Ampache Server '%s'.\nConnect using these credentials? [Y/n]: " % (username, url)) if resp != 'n' and resp != 'N': # then use the credentials ampache_conn.authenticate() ready = True while not ready: url = raw_input("Ampache Server URL (ie 'http://example.org/ampache'): ") username = raw_input("Username: ") password = getpass("Password: ") ampache_conn.set_credentials(username, password, url) if ampache_conn.has_credentials(): if ampache_conn.authenticate(): ready = True else: print "Error! Try Again" for artist in ampache_conn.get_artists(): print "%d: %s" % (artist['artist_id'], artist['artist_name']) artist_id = get_valid_digit("Artist ID: ") print '\n' for album in ampache_conn.get_albums_by_artist(artist_id): print "%d: %s (Year = %s, Disk = %d, Tracks = %d)" % (album['album_id'], album['album_name'], album['album_year'], album['album_disk'], album['album_tracks']) album_id = get_valid_digit("Album ID: ") print '\n' song_list = ampache_conn.get_songs_by_album(album_id) song_list = sorted(song_list, key=lambda k: k['song_track']) for song in song_list: print "%d: %s" % (song['song_track'], song['song_title']) list = [] for song in song_list: list.append(song['song_id']) song_track = get_valid_digit("Track Number: ") audio_engine.play_from_list_of_songs(list, int(song_track)-1) print_now_playing() quit = False while not quit: print "Choices are 'n' for next, 'p' for previous, 'i' for info, 's' for play/pause, and 'q' for quit" resp = raw_input("Choice: ") print "\n\n" if resp == 'n': next_track() elif resp == 'p': prev_track() elif resp == 'i': print_now_playing() elif resp == 's': if audio_engine.get_state() == 'playing': audio_engine.pause() print audio_engine.get_state() else: audio_engine.play() print audio_engine.get_state() elif resp == 'q': quit = True print "\n\n" viridian-1.2/AmpacheTools/AmpacheSession.py0000755000175000017500000006407211511672746020642 0ustar charliecharlie#!/usr/bin/env python # -*- coding: utf-8 -*- ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE import os import hashlib import time import xml.dom.minidom import urllib2 import urllib import datetime import re import socket import sys, traceback ### Constants ### AUTH_MAX_RETRY = 3 # how many times to try and reauth before failure DEFAULT_TIMEOUT = 10 # default 10 second timeout __ILLEGAL_XML = u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' + \ u'|' + \ u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])' % \ (unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff), unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff), unichr(0xd800),unichr(0xdbff),unichr(0xdc00),unichr(0xdfff)) ILLEGAL_XML_RE = re.compile(__ILLEGAL_XML) socket.setdefaulttimeout(DEFAULT_TIMEOUT) class AmpacheSession: """ The AmpacheSession Class. This is used to communicate to Ampache via the API. """ def __init__(self): """ Initialize an AmpacheSession. """ self.url = None self.username = None self.password = None self.xml_rpc = None self.auth = None self.last_update_time = -1 self.artists_num = -1 self.albums_num = -1 self.songs_num = -1 self.auth_current_retry = 0 def set_credentials(self, username, password, url): """ Save the ampache url, username, and password. """ # remove trailing slash off URL if url != None: while ( url[-1:] == '/' ): url = url[:-1] # save variables to object self.url = url self.username = username self.password = password try: self.xml_rpc = self.url + "/server/xml.server.php" except: pass def get_credentials(self): """ Retrun the url, username, and password as a tuple. """ return (self.username, self.password, self.url) def has_credentials(self): """ Checks to see if the AmpacheSession object has credentials set. """ if self.username == None or self.password == None or self.url == None or self.xml_rpc == None: return False elif self.username == "" or self.password == "" or self.url == "" or self.xml_rpc == "": return False return True def authenticate(self): """ Attempt to authenticate to Ampache. Returns True if successful and False if not. This will retry AUTH_MAX_RETRY(=3) times. """ # check for the necessary information if not self.has_credentials(): return False # generate the necessary information for the authentication timestamp = int(time.time()) password = hashlib.sha256(self.password).hexdigest() authkey = hashlib.sha256(str(timestamp) + password).hexdigest() values = {'action' : 'handshake', 'auth' : authkey, 'timestamp' : timestamp, 'user' : self.username, 'version' : '350001', } data = urllib.urlencode(values) # now send the authentication request to Ampache try: socket.setdefaulttimeout(7) # lower timeout response = urllib2.urlopen(self.xml_rpc + '?' + data) socket.setdefaulttimeout(DEFAULT_TIMEOUT) # reset timeout xml_string = response.read() dom = xml.dom.minidom.parseString(xml_string) self.auth = dom.getElementsByTagName("auth")[0].childNodes[0].data self.artists_num = int(dom.getElementsByTagName("artists")[0].childNodes[0].data) self.albums_num = int(dom.getElementsByTagName("albums")[0].childNodes[0].data) self.songs_num = int(dom.getElementsByTagName("songs")[0].childNodes[0].data) except: # couldn't auth, try up to AUTH_MAX_RETRY times self.auth = None self.auth_current_retry += 1 print "[Error] Authentication Failed -- Retry = %d" % self.auth_current_retry if ( self.auth_current_retry < AUTH_MAX_RETRY ): return self.authenticate() else: # authentication failed more than AUTH_MAX_RETRY times self.auth_current_retry = 0 error = None try: # to find the error error = dom.getElementsByTagName("error")[0].childNodes[0].data print "[Error] Authentication Failed :: %s" % error return error except: # no error found.. must have failed because data was sent to wrong place return False # if it made it this far, the auth was successfull, now check to see if the catalog needs updating try: # check to see if ampache has been updated or cleaned since the last time this ran update = dom.getElementsByTagName("update")[0].childNodes[0].data add = dom.getElementsByTagName("add")[0].childNodes[0].data clean = dom.getElementsByTagName("clean")[0].childNodes[0].data # convert ISO 8601 to epoch update = int(time.mktime(time.strptime( update[:-6], "%Y-%m-%dT%H:%M:%S" ))) add = int(time.mktime(time.strptime( add[:-6], "%Y-%m-%dT%H:%M:%S" ))) clean = int(time.mktime(time.strptime( clean[:-6], "%Y-%m-%dT%H:%M:%S" ))) new_time = max([update, add, clean]) self.last_update_time = new_time except Exception, detail: print "Couldn't get time catalog was updated -- assuming catalog is dirty -- ", detail self.last_update_time = -1 self.auth_current_retry = 0 return True def is_authenticated(self): """ Returns True if self.auth is set, and False if it is not. """ if self.auth != None: return True return False def ping(self): """ Ping extends the current session to Ampache. Returns None if it fails, or the time the session expires if it is succesful """ values = {'action' : 'ping', 'auth' : self.auth, } root = self.__call_api(values) if not root: return None session = root.getElementsByTagName('session_expire')[0].childNodes[0].data return session ####################################### # Public Getter Methods ####################################### def get_last_update_time(self): """ Returns the last time the catalog on the Ampache server was updated. """ return self.last_update_time def get_song_url(self, song_id): """ Takes a song_id and returns the url to the song (with the current authentication). """ values = {'action' : 'song', 'filter' : song_id, 'auth' : self.auth, } root = self.__call_api(values) if not root: return None song = root.getElementsByTagName('song')[0] song_url = song.getElementsByTagName('url')[0].childNodes[0].data return song_url def get_album_art(self, album_id): """ Takes an album_id and returns the url to the artwork (with the current authentication). """ if album_id == None: return None values = {'action' : 'album', 'filter' : album_id, 'auth' : self.auth, } root = self.__call_api(values) if not root: return None album = root.getElementsByTagName('album')[0] album_art = album.getElementsByTagName('art')[0].childNodes[0].data return album_art def get_artists(self, offset=None): """ Gets all artists and return as a list of dictionaries. Example: [ { 'artist_id' : artist_id, 'artist_name' : artist_name}, { 'artist_id' : 1, 'artist_name' : 'The Reign of Kindo'}, { ... }, ] """ if offset == None: if self.artists_num > 5000: # offset needed print "More than 5000 artists" list = [] for i in range(0, self.artists_num, 5000): print "Offset = ", i list += self.get_artists(i) return list values = {'action' : 'artists', 'auth' : self.auth, } else: values = {'action' : 'artists', 'auth' : self.auth, 'offset' : offset, } root = self.__call_api(values) if not root: return None nodes = root.getElementsByTagName('artist') list = [] try: # get the artists for child in nodes: artist_name = child.getElementsByTagName('name')[0].childNodes[0].data artist_id = int(child.getAttribute('id')) dict = { 'artist_id' : artist_id, 'artist_name' : artist_name, } list.append( dict ) except: # something failed traceback.print_exc() return None return list def get_albums(self, offset=None): """ Gets all albums and return as a list of dictionaries. Example: [ { 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'album_year' : album_year, 'album_tracks' : album_tracks, 'album_disk' : album_disk, 'album_rating' : album_rating, 'precise_rating' : precise_rating, }, { ... }, ] """ if offset == None: if self.albums_num > 5000: # offset needed list = [] for i in range(0, self.albums_num, 5000): list += self.get_artists(i) return list values = {'action' : 'albums', 'auth' : self.auth, } else: values = {'action' : 'albums', 'auth' : self.auth, 'offset' : offset, } root = self.__call_api(values) if not root: return None nodes = root.getElementsByTagName('album') if not nodes: return None list = [] try: for child in nodes: album_id = int(child.getAttribute('id')) album_name = child.getElementsByTagName('name')[0].childNodes[0].data artist_id = int(child.getElementsByTagName('artist')[0].getAttribute('id')) artist_name = child.getElementsByTagName('artist')[0].childNodes[0].data album_year = child.getElementsByTagName('year')[0].childNodes[0].data album_tracks = int(child.getElementsByTagName('tracks')[0].childNodes[0].data) album_disk = int(child.getElementsByTagName('disk')[0].childNodes[0].data) try: # new version doesn't put data in the middle... precise_rating = int(child.getElementsByTagName('preciserating')[0].childNodes[0].data) except: precise_rating = 0 try: album_rating = child.getElementsByTagName('rating')[0].childNodes[0].data except: album_rating = 0 if album_year == "N/A": album_year = 0 album_year = int(album_year) dict = { 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'album_year' : album_year, 'album_tracks' : album_tracks, 'album_disk' : album_disk, 'album_rating' : album_rating, 'precise_rating' : precise_rating, } list.append( dict ) except: #something failed traceback.print_exc() return None return list def get_songs(self, offset=None): """ Gets all songs and returns as a list of dictionaries. Example: [ { 'song_id' : song_id, 'song_title' : song_title, 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'song_track' : song_track, 'song_time' : song_time, 'song_size' : song_size, 'precise_rating' : precise_rating, 'rating' : rating, 'art' : art, 'url' : url, }, { ... }, ] """ if offset == None: if self.songs_num > 100: # offset needed print "over 5000" list = [] for i in range(0, self.songs_num, 100): list += self.get_songs(i) return list values = {'action' : 'songs', 'auth' : self.auth, } else: values = {'action' : 'songs', 'auth' : self.auth, 'offset' : offset, } print values root = self.__call_api(values) if not root: return None nodes = root.getElementsByTagName('song') if not nodes: return None list = [] try: for song in nodes: song_id = int(song.getAttribute('id')) song_title = song.getElementsByTagName('title')[0].childNodes[0].data artist_id = int(song.getElementsByTagName('artist')[0].getAttribute('id')) artist_name = song.getElementsByTagName('artist')[0].childNodes[0].data album_id = int(song.getElementsByTagName('album')[0].getAttribute('id')) album_name = song.getElementsByTagName('album')[0].childNodes[0].data song_track = int(song.getElementsByTagName('track')[0].childNodes[0].data) song_time = int(song.getElementsByTagName('time')[0].childNodes[0].data) song_size = int(song.getElementsByTagName('size')[0].childNodes[0].data) try: # New version doesn't initialize this... precise_rating = int(song.getElementsByTagName('preciserating')[0].childNodes[0].data) except: precise_rating = 0 try: rating = float(song.getElementsByTagName('rating')[0].childNodes[0].data) except: rating = 0 art = song.getElementsByTagName('art')[0].childNodes[0].data url = song.getElementsByTagName('url')[0].childNodes[0].data dict = { 'song_id' : song_id, 'song_title' : song_title, 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'song_track' : song_track, 'song_time' : song_time, 'song_size' : song_size, 'precise_rating' : precise_rating, 'rating' : rating, 'art' : art, 'url' : url, } list.append( dict ) except: traceback.print_exc() return None return list def get_albums_by_artist(self, artist_id): """ Gets all albums by the artist_id and returns as a list of dictionaries. Example: [ { 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'album_year' : album_year, 'album_tracks' : album_tracks, 'album_disk' : album_disk, 'album_rating' : album_rating, 'precise_rating' : precise_rating, }, { ... }, ] """ values = {'action' : 'artist_albums', 'filter' : artist_id, 'auth' : self.auth, } root = self.__call_api(values) nodes = root.getElementsByTagName('album') if not nodes: return None list = [] try: for child in nodes: album_id = int(child.getAttribute('id')) album_name = child.getElementsByTagName('name')[0].childNodes[0].data artist_id = int(child.getElementsByTagName('artist')[0].getAttribute('id')) artist_name = child.getElementsByTagName('artist')[0].childNodes[0].data album_year = child.getElementsByTagName('year')[0].childNodes[0].data album_tracks = int(child.getElementsByTagName('tracks')[0].childNodes[0].data) try: album_disk = int(child.getElementsByTagName('disk')[0].childNodes[0].data) except: album_disk = 0 try: precise_rating = int(child.getElementsByTagName('preciserating')[0].childNodes[0].data) except: precise_rating = 0 try: album_rating = child.getElementsByTagName('rating')[0].childNodes[0].data except: album_rating = 0 if album_year == "N/A": album_year = 0 album_year = int(album_year) dict = { 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'album_year' : album_year, 'album_tracks' : album_tracks, 'album_disk' : album_disk, 'album_rating' : album_rating, 'precise_rating' : precise_rating, } list.append( dict ) except: #something failed print "This artist failed", artist_id traceback.print_exc() return None return list def get_songs_by_album(self, album_id): """ Gets all songs on album_id and returns as a list of dictionaries. Example: [ { 'song_id' : song_id, 'song_title' : song_title, 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'song_track' : song_track, 'song_time' : song_time, 'song_size' : song_size, 'precise_rating' : precise_rating, 'rating' : rating, 'art' : art, 'url' : url, }, { ... }, ] """ values = {'action' : 'album_songs', 'filter' : album_id, 'auth' : self.auth, } root = self.__call_api(values) nodes = root.getElementsByTagName('song') if not nodes: # list is empty, reauth return None list = [] try: for song in nodes: song_id = int(song.getAttribute('id')) song_title = song.getElementsByTagName('title')[0].childNodes[0].data artist_id = int(song.getElementsByTagName('artist')[0].getAttribute('id')) artist_name = song.getElementsByTagName('artist')[0].childNodes[0].data album_id = int(song.getElementsByTagName('album')[0].getAttribute('id')) album_name = song.getElementsByTagName('album')[0].childNodes[0].data song_track = int(song.getElementsByTagName('track')[0].childNodes[0].data) song_time = int(song.getElementsByTagName('time')[0].childNodes[0].data) song_size = int(song.getElementsByTagName('size')[0].childNodes[0].data) try: # New version doesn't initialize this... precise_rating = int(song.getElementsByTagName('preciserating')[0].childNodes[0].data) except: precise_rating = 0 try: rating = float(song.getElementsByTagName('rating')[0].childNodes[0].data) except: rating = 0 art = song.getElementsByTagName('art')[0].childNodes[0].data url = song.getElementsByTagName('url')[0].childNodes[0].data dict = { 'song_id' : song_id, 'song_title' : song_title, 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'song_track' : song_track, 'song_time' : song_time, 'song_size' : song_size, 'precise_rating' : precise_rating, 'rating' : rating, 'art' : art, 'url' : url, } list.append( dict ) except: print "This album failed", album_id traceback.print_exc() return None return list def get_song_info(self, song_id): """ Gets all info about a song from the song_id and returns it as a dictionary. Example: { 'song_id' : song_id, 'song_title' : song_title, 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'song_track' : song_track, 'song_time' : song_time, 'song_size' : song_size, 'precise_rating' : precise_rating, 'rating' : rating, 'art' : art, 'url' : url, } """ values = {'action' : 'song', 'filter' : song_id, 'auth' : self.auth, } root = self.__call_api(values) song = root.getElementsByTagName('song')[0] if not song: return None song_dict = {} try: song_id = int(song.getAttribute('id')) song_title = song.getElementsByTagName('title')[0].childNodes[0].data artist_id = int(song.getElementsByTagName('artist')[0].getAttribute('id')) artist_name = song.getElementsByTagName('artist')[0].childNodes[0].data album_id = int(song.getElementsByTagName('album')[0].getAttribute('id')) album_name = song.getElementsByTagName('album')[0].childNodes[0].data song_track = int(song.getElementsByTagName('track')[0].childNodes[0].data) song_time = int(song.getElementsByTagName('time')[0].childNodes[0].data) song_size = int(song.getElementsByTagName('size')[0].childNodes[0].data) try: # New version doesn't set this... precise_rating = int(song.getElementsByTagName('preciserating')[0].childNodes[0].data) except: precise_rating = 0 try: rating = float(song.getElementsByTagName('rating')[0].childNodes[0].data) except: rating = 0 art = song.getElementsByTagName('art')[0].childNodes[0].data url = song.getElementsByTagName('url')[0].childNodes[0].data song_dict = { 'song_id' : song_id, 'song_title' : song_title, 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'song_track' : song_track, 'song_time' : song_time, 'song_size' : song_size, 'precise_rating' : precise_rating, 'rating' : rating, 'art' : art, 'url' : url, } except: print "This song failed", song_id traceback.print_exc() return None return song_dict def get_playlists(self): """ Gets a list of all of the playlists on the server. Example: [ { 'id' : id, 'owner' : owner, 'name' : name, 'items' : items, 'type' : type, }, { ... }, ] """ values = {'action' : 'playlists', 'auth' : self.auth, } root = self.__call_api(values) nodes = root.getElementsByTagName('playlist') if not nodes: # list is empty, reauth return None list = [] try: for child in nodes: id = int(child.getAttribute('id')) name = child.getElementsByTagName('name')[0].childNodes[0].data owner = child.getElementsByTagName('owner')[0].childNodes[0].data items = int(child.getElementsByTagName('items')[0].childNodes[0].data) type = child.getElementsByTagName('type')[0].childNodes[0].data dict = { 'id' : id, 'name' : name, 'items' : items, 'owner' : owner, 'type' : type, } list.append( dict ) except: #something failed traceback.print_exc() return [] return list def get_playlist_songs(self, playlist_id): """ Gets all info about a song from the song_id and returns it as a dictionary. Example: [ { 'song_id' : song_id, 'song_title' : song_title, 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'song_track' : song_track, 'song_time' : song_time, 'song_size' : song_size, 'precise_rating' : precise_rating, 'rating' : rating, 'art' : art, 'url' : url, }, {...} ] """ values = {'action' : 'playlist_songs', 'filter' : playlist_id, 'auth' : self.auth, } root = self.__call_api(values) songs = root.getElementsByTagName('song') if not songs: return None list = [] try: for song in songs: song_id = int(song.getAttribute('id')) song_title = song.getElementsByTagName('title')[0].childNodes[0].data artist_id = int(song.getElementsByTagName('artist')[0].getAttribute('id')) artist_name = song.getElementsByTagName('artist')[0].childNodes[0].data album_id = int(song.getElementsByTagName('album')[0].getAttribute('id')) album_name = song.getElementsByTagName('album')[0].childNodes[0].data song_track = int(song.getElementsByTagName('track')[0].childNodes[0].data) song_time = int(song.getElementsByTagName('time')[0].childNodes[0].data) song_size = int(song.getElementsByTagName('size')[0].childNodes[0].data) try: # New Ampache puts nothing here... precise_rating = int(song.getElementsByTagName('preciserating')[0].childNodes[0].data) except: precise_rating = 0 try: rating = float(song.getElementsByTagName('rating')[0].childNodes[0].data) except: rating = 0 art = song.getElementsByTagName('art')[0].childNodes[0].data url = song.getElementsByTagName('url')[0].childNodes[0].data song_dict = { 'song_id' : song_id, 'song_title' : song_title, 'artist_id' : artist_id, 'artist_name' : artist_name, 'album_id' : album_id, 'album_name' : album_name, 'song_track' : song_track, 'song_time' : song_time, 'song_size' : song_size, 'precise_rating' : precise_rating, 'rating' : rating, 'art' : art, 'url' : url, } list.append(song_dict) except: print "This playlist failed", playlist_id traceback.print_exc() return None return list def __call(self, **kwargs): """Takes kwargs and talks to the ampach API.. returning the root element of the XML Example: __call(action="artists", filter="kindo") """ values = kwargs return self.__call_api(values) def __call_api(self, values): """Takes a dictionary of values and talks to the ampache API... returning the root elemnent of the XML Example: __call_api({action: 'artists', filter: 'kindo'}) Automatically adds {auth: }""" values['auth'] = self.auth data = urllib.urlencode(values) try: # to query ampache response = urllib2.urlopen(self.xml_rpc + '?' + data) x = self.__sanatize(response.read()) dom = xml.dom.minidom.parseString(x) except: # The data pulled from Ampache was invalid traceback.print_exc() return None try: # to make sure authentication is valid and extract the root element root = dom.getElementsByTagName('root')[0] if not root: # list is empty, reauth raise Exception('Reauthenticate') else: # try to find an error try: error = root.getElementsByTagName("error")[0].childNodes[0].data print "Error! Trying to reauthenticate :: %s" % error if self.authenticate(): return self.__call_api(values) return None except: # no error found.. must be good XML :) return root except: # something failed, try to reauth and do it again if self.authenticate(): return self.__call_api(values) else: # couldn't authenticate return None return None def __sanatize(self, string): """Sanatize the given string to remove bad characters.""" # from http://boodebr.org/main/python/all-about-python-and-unicode#UNI_XML for match in ILLEGAL_XML_RE.finditer(string): string = string[:match.start()] + "?" + string[match.end():] string = string.replace('—', '-') try: # try to encode the whole string to UTF-8 string2 = string.encode("utf-8") except: # if it fails try it character by character, stripping out bad characters string2 = "" for c in string: try: a = c.encode("utf-8") string2 += a except: string2 += '?' return string2 viridian-1.2/AmpacheTools/guifunctions.py0000755000175000017500000000457111511672746020453 0ustar charliecharlie#!/usr/bin/env python # -*- coding: utf-8 -*- ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE import pygtk pygtk.require("2.0") import gtk import os """ GTK Helper functions """ def create_single_column_tree_view(column_name, model, sort_column=None): """Create a treeview by passing a column_name and a model (gtk.ListStore()).""" tree_view = gtk.TreeView(model) tree_view.set_rules_hint(True) column = create_column(column_name, 0, sort_column) tree_view.append_column(column) return tree_view def create_column(column_name, column_id, sort_column=None, pixbuf=False): """Helper function for treeviews, this will return a column ready to be appended.""" if pixbuf: renderer_text = gtk.CellRendererPixbuf() column = gtk.TreeViewColumn(column_name) column.pack_start(renderer_text, expand=False) column.add_attribute(renderer_text, 'pixbuf', 0) else: renderer_text = gtk.CellRendererText() column = gtk.TreeViewColumn(column_name, renderer_text, text=column_id) if sort_column != None: column.set_sort_column_id(sort_column) else: column.set_sort_column_id(column_id) return column def create_image_pixbuf(file, width, height=None): """Helper function to create a pixel buffer from a file of a set width and height.""" if height == None: height = width image = gtk.gdk.pixbuf_new_from_file(file).scale_simple(width, height, gtk.gdk.INTERP_BILINEAR) return image def hyperlink(url, text=None): """Returns a button that acts as a hyperlink.""" if text == None: text = url label = gtk.Label(""+text+"") label.set_use_markup(True) button = gtk.Button() button.add(label) button.set_relief(gtk.RELIEF_NONE) button.connect('clicked', lambda x_: os.popen("gnome-open '%s' &" % (url))) return buttonviridian-1.2/AmpacheTools/DatabaseSession.py0000755000175000017500000000477211511672746021011 0ustar charliecharlie#!/usr/bin/env python # -*- coding: utf-8 -*- ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE import os import cPickle try: import sqlite3 except: print "[Warn] sqlite3 not found -- loading sqlite2" from pysqlite2 import dbapi2 as sqlite3 class DatabaseSession: """ A class to access and modify a sqlite database. """ def __init__(self, database): """ Initialize the database session and create a `variable` table. """ self.db_conn = sqlite3.connect(database) c = self.cursor() c.execute('''CREATE TABLE IF NOT EXISTS variable (name text NOT NULL DEFAULT '', value text NOT NULL DEFAULT '' ) ''') self.commit() c.close() def cursor(self): """ Returns a cursor to the database. """ return self.db_conn.cursor() def commit(self): """ Commits the database. """ self.db_conn.commit() def table_is_empty(self, table_name): """ Returns True if the table is empty. """ c = self.cursor() c.execute("""SELECT 1 FROM %s LIMIT 1""" % table_name) result = c.fetchone() #self.commit() c.close() if result == None: return True # empty return False def variable_set(self, var_name, var_value): """ Save a variable in the database. """ #var_value = self.__convert_specials_to_strings(var_value) c = self.cursor() c.execute("""DELETE FROM variable WHERE name = ?""", [var_name]) c.execute("""INSERT INTO variable (name, value) VALUES (?, ?)""", [var_name, str(cPickle.dumps(var_value))]) self.commit() c.close() def variable_get(self, var_name, default_value=None): """ Retrieve a variable from the database. """ try: c = self.cursor() c.execute("""SELECT value FROM variable WHERE name = ?""", [var_name]) #result = self.__convert_strings_to_specials(c.fetchone()[0]) result = c.fetchone()[0] #self.commit() c.close() except: c.close() return default_value return cPickle.loads(str(result)) viridian-1.2/AmpacheTools/__init__.py0000755000175000017500000000026211511672746017466 0ustar charliecharlie#!/usr/bin/env python from AmpacheSession import AmpacheSession from AmpacheGUI import AmpacheGUI from AudioEngine import AudioEngine from DatabaseSession import DatabaseSession viridian-1.2/AmpacheTools/AmpacheGUI.py0000755000175000017500000035270611511672746017647 0ustar charliecharlie#!/usr/bin/env python # -*- coding: utf-8 -*- ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE import sys import os import urllib2 import time import thread import gobject import urllib import re import shutil import cPickle import getpass import traceback try: import glib GLIB_AVAILABLE = True except ImportError: GLIB_AVAILABLE = False try: # require pygtk import pygtk pygtk.require("2.0") import gtk except: print "pygtk required!" sys.exit(1); try: # check for dbus import dbus DBUS_AVAILABLE = True except ImportError: DBUS_AVAILABLE = False try: # check for pynotify import pynotify pynotify.init('Viridian') pynotify_object = pynotify.Notification(" ", " ") PYNOTIFY_INSTALLED = True except: PYNOTIFY_INSTALLED = False # personal helper functions import dbfunctions import helperfunctions import guifunctions from XMLRPCServerSession import XMLServer ### Contstants ### ALBUM_ART_SIZE = 90 SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__)) PLUGINS_DIR = os.path.join(SCRIPT_PATH, 'plugins/') IMAGES_DIR = os.path.join(SCRIPT_PATH, 'images/') THREAD_LOCK = thread.allocate_lock() VIRIDIAN_DIR = os.path.join(os.path.expanduser("~"), '.viridian') ALBUM_ART_DIR = os.path.join(VIRIDIAN_DIR, 'album_art') XML_RPC_PORT = 4596 VERSION_NUMBER = "" class AmpacheGUI: """The Ampache GUI Class""" def main(self): """Method to call gtk.main() and display the GUI.""" gobject.threads_init() gobject.idle_add(self.main_gui_callback) ### Status tray icon #### self.tray_icon_to_display = self.db_session.variable_get('tray_icon_to_display', 'standard') if self.tray_icon_to_display == "standard": self.tray_icon = gtk.StatusIcon() self.tray_icon.set_from_pixbuf(self.images_pixbuf_viridian_app) self.tray_icon.connect('activate', self.status_icon_activate) self.tray_icon.connect('popup-menu', self.status_icon_popup_menu) self.tray_icon.set_tooltip('Viridian') ### Seek Bar Thread (1/4 second) ### gobject.timeout_add(250, self.query_position) ### Keep session active when song is paused (ping every minute) ### #gobject.timeout_add(1000 * 60, self.keep_session_active) gtk.main() def delete_event(self, widget, event, data=None): """Keep the window alive when it is X'd out.""" if not hasattr(self, 'tray_icon') or self.quit_when_window_closed: # no tray icon set, must destroy self.destroy() else: # don't quit, just hide if self.first_time_closing: self.main_gui_toggle_hidden() self.create_dialog_alert("info", _("Viridian is still running in the status bar. If you do not want Viridian to continue running when the window is closed you can disable it in Preferences."), True) self.first_time_closing = False self.db_session.variable_set('first_time_closing', False) else: self.main_gui_toggle_hidden() return True def destroy(self, widget=None, data=None): """The function when the program exits.""" if THREAD_LOCK.locked(): result = self.create_dialog_ok_or_close(_("Downloads in progress.."), _("There are unfinished downloads, are you sure you want to quit?")) if result != "ok": return True self.stop_all_threads() size = self.window.get_size() self.stop_xml_server() self.db_session.variable_set('current_playlist', self.audio_engine.get_playlist()) self.db_session.variable_set('volume', self.audio_engine.get_volume()) self.db_session.variable_set('window_size_width', size[0]) self.db_session.variable_set('window_size_height', size[1]) gtk.main_quit() def __init__(self, ampache_conn, audio_engine, db_session, is_first_time, version): """Constructor for the AmpacheGUI Class. Takes an AmpacheSession Object, an AudioEngine Object and a DatabaseSession Object.""" ################################# # Set Variables ################################# global VERSION_NUMBER VERSION_NUMBER = version self.audio_engine = audio_engine self.ampache_conn = ampache_conn self.db_session = db_session plugins_list = self.__find_plugins(PLUGINS_DIR) self.plugins = {} for plugin_name in plugins_list: plugin = self.__import_plugin(plugin_name) if plugin != None: self.plugins[plugin_name] = plugin print "Plugins = ", self.plugins # DEBUG self.enabled_plugins = self.db_session.variable_get('enabled_plugins', []) xmlrpc_port = self.db_session.variable_get('xmlrpc_port', XML_RPC_PORT) self.xml_server = XMLServer('', xmlrpc_port) self.is_first_time = is_first_time self.catalog_up_to_date = None self.current_song_info = None self.tree_view_dict = {} dbfunctions.create_initial_tables(self.db_session) volume = self.db_session.variable_get('volume', float(100)) width = self.db_session.variable_get('window_size_width', 1150) height = self.db_session.variable_get('window_size_height', 600) ################################## # Load Images ################################## self.images_pixbuf_play = guifunctions.create_image_pixbuf(IMAGES_DIR + 'play.png', 75) self.images_pixbuf_pause = guifunctions.create_image_pixbuf(IMAGES_DIR + 'pause.png', 75) self.images_pixbuf_gold_star = guifunctions.create_image_pixbuf(IMAGES_DIR + 'star_rating_gold.png', 16) self.images_pixbuf_gray_star = guifunctions.create_image_pixbuf(IMAGES_DIR + 'star_rating_gray.png', 16) images_pixbuf_prev = guifunctions.create_image_pixbuf(IMAGES_DIR + 'prev.png', 75) images_pixbuf_next = guifunctions.create_image_pixbuf(IMAGES_DIR + 'next.png', 75) self.images_pixbuf_playing = guifunctions.create_image_pixbuf(IMAGES_DIR + 'playing.png', 15) self.images_pixbuf_empty = guifunctions.create_image_pixbuf(IMAGES_DIR + 'empty.png', 1) self.images_pixbuf_viridian_simple = guifunctions.create_image_pixbuf(IMAGES_DIR + 'ViridianSimple.png', 20) self.images_pixbuf_viridian_app = guifunctions.create_image_pixbuf(IMAGES_DIR + 'ViridianApp.png', 70) ################################## # Main Window ################################## self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.connect("delete_event", self.delete_event) self.window.connect("destroy", self.destroy) self.window.set_title("Viridian") self.window.resize(width, height) self.window.set_icon(self.images_pixbuf_viridian_simple) main_box = gtk.VBox() ################################# # Menu Bar ################################# menu_bar = gtk.MenuBar() agr = gtk.AccelGroup() self.window.add_accel_group(agr) """Start File Menu""" file_menu = gtk.Menu() filem = gtk.MenuItem(_("_File")) filem.set_submenu(file_menu) newi = gtk.MenuItem(_("Reauthenticate")) newi.connect("activate", self.button_reauthenticate_clicked) file_menu.append(newi) self.go_to_ampache_menu_item = gtk.MenuItem(_("Open Ampache")) self.go_to_ampache_menu_item.connect("activate", lambda x: self.gnome_open(self.ampache_conn.url)) self.go_to_ampache_menu_item.set_sensitive(False) file_menu.append(self.go_to_ampache_menu_item) sep = gtk.SeparatorMenuItem() file_menu.append(sep) newi = gtk.ImageMenuItem(_("Save Playlist"), agr) img = gtk.image_new_from_stock(gtk.STOCK_SAVE, gtk.ICON_SIZE_MENU) newi.set_image(img) key, mod = gtk.accelerator_parse("S") newi.add_accelerator("activate", agr, key, mod, gtk.ACCEL_VISIBLE) newi.connect("activate", self.button_save_playlist_clicked) file_menu.append(newi) newi = gtk.ImageMenuItem(_("Load Playlist"), agr) img = gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU) newi.set_image(img) key, mod = gtk.accelerator_parse("O") newi.add_accelerator("activate", agr, key, mod, gtk.ACCEL_VISIBLE) newi.connect("activate", self.button_load_playlist_clicked) file_menu.append(newi) newi = gtk.ImageMenuItem(_("Export Playlist..."))#, agr) img = gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU) newi.set_image(img) #key, mod = gtk.accelerator_parse("E") #newi.add_accelerator("activate", agr, key, mod, gtk.ACCEL_VISIBLE) newi.connect("activate", self.button_export_playlist_clicked) file_menu.append(newi) sep = gtk.SeparatorMenuItem() file_menu.append(sep) newi = gtk.MenuItem(_("Clear Album Art")) newi.connect("activate", self.button_clear_album_art_clicked) file_menu.append(newi) newi = gtk.MenuItem(_("Clear Local Cache")) newi.connect("activate", self.button_clear_cached_artist_info_clicked) file_menu.append(newi) newi = gtk.MenuItem(_("Pre-Cache")) newi.connect("activate", self.button_pre_cache_info_clicked) file_menu.append(newi) sep = gtk.SeparatorMenuItem() file_menu.append(sep) exit = gtk.ImageMenuItem(gtk.STOCK_QUIT, agr) key, mod = gtk.accelerator_parse("Q") exit.add_accelerator("activate", agr, key, mod, gtk.ACCEL_VISIBLE) exit.connect("activate", self.destroy) file_menu.append(exit) menu_bar.append(filem) """End File Menu""" """Start Edit Menu""" edit_menu = gtk.Menu() editm = gtk.MenuItem(_("_Edit")) editm.set_submenu(edit_menu) newi = gtk.MenuItem(_("Plugins")) newi.connect("activate", self.show_plugins_window) edit_menu.append(newi) newi = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES, agr) key, mod = gtk.accelerator_parse("P") newi.add_accelerator("activate", agr, key, mod, gtk.ACCEL_VISIBLE) newi.connect("activate", self.show_settings) edit_menu.append(newi) menu_bar.append(editm) """End Edit Menu""" """Start View Menu""" view_menu = gtk.Menu() viewm = gtk.MenuItem(_("_View")) viewm.set_submenu(view_menu) newi = gtk.CheckMenuItem(_("Show Playlist")) show_playlist = self.db_session.variable_get('show_playlist', True) newi.set_active(show_playlist) newi.connect("activate", self.toggle_playlist_view) view_menu.append(newi) self.show_downloads_checkbox = gtk.CheckMenuItem(_("Show Downloads")) show_downloads = self.db_session.variable_get('show_downloads', False) self.show_downloads_checkbox.set_active(show_downloads) self.show_downloads_checkbox.connect("activate", self.toggle_downloads_view) view_menu.append(self.show_downloads_checkbox) sep = gtk.SeparatorMenuItem() view_menu.append(sep) newi = gtk.CheckMenuItem(_("View Statusbar")) view_statusbar = self.db_session.variable_get('view_statusbar', True) newi.set_active(view_statusbar) newi.connect("activate", self.toggle_statusbar_view) view_menu.append(newi) menu_bar.append(viewm) """End View Menu""" """Start Help Menu""" help_menu = gtk.Menu() helpm = gtk.MenuItem(_("_Help")) helpm.set_submenu(help_menu) newi = gtk.ImageMenuItem(gtk.STOCK_HELP) newi.connect("activate", self.show_help) help_menu.append(newi) newi = gtk.ImageMenuItem(gtk.STOCK_ABOUT) newi.connect("activate", self.create_about_dialog) help_menu.append(newi) menu_bar.append(helpm) """End Help Menu""" vbox = gtk.VBox(False, 2) vbox.pack_start(menu_bar, False, False, 0) main_box.pack_start(vbox, False, False, 0) """End Menu Bar""" ################################# # Top Control Bar ################################# top_bar = gtk.HBox() top_bar_left = gtk.VBox() top_bar_left_top = gtk.HBox() top_bar_left_bottom = gtk.HBox() ### Prev Button prev_image = gtk.Image() prev_image.set_from_pixbuf(images_pixbuf_prev) event_box_prev = gtk.EventBox() event_box_prev.connect("button_release_event", self.button_prev_clicked) event_box_prev.add(prev_image) ### Play/Pause Button self.play_pause_image = gtk.Image() self.play_pause_image.set_from_pixbuf(self.images_pixbuf_play) event_box_play = gtk.EventBox() event_box_play.connect("button_release_event", self.button_play_pause_clicked) event_box_play.add(self.play_pause_image) next_image = gtk.Image() next_image.set_from_pixbuf(images_pixbuf_next) event_box_next = gtk.EventBox() event_box_next.connect("button_release_event", self.button_next_clicked) event_box_next.add(next_image) top_bar_left_top.pack_start(event_box_prev, False, False, 0) top_bar_left_top.pack_start(event_box_play, False, False, 0) top_bar_left_top.pack_start(event_box_next, False, False, 0) ### Volume slider, repeat songs self.volume_slider = gtk.HScale() self.volume_slider.set_inverted(False) self.volume_slider.set_range(0, 100) self.volume_slider.set_increments(1, 10) self.volume_slider.set_draw_value(False) self.volume_slider.connect('change-value', self.on_volume_slider_change) self.volume_slider.set_size_request(80, 20) self.volume_slider.set_value(volume) repeat_songs_checkbutton = gtk.CheckButton(_("Repeat")) repeat_songs_checkbutton.set_active(False) repeat_songs_checkbutton.connect("toggled", self.toggle_repeat_songs) shuffle_songs_checkbutton = gtk.CheckButton(_("Shuffle")) shuffle_songs_checkbutton.set_active(False) shuffle_songs_checkbutton.connect("toggled", self.toggle_shuffle_songs) hbox = gtk.HBox() vbox = gtk.VBox() label = gtk.Label() label.set_markup(_('Volume')) vbox.pack_start(label, False, False, 0) vbox.pack_start(self.volume_slider, False, False, 0) hbox.pack_start(vbox, False, False, 0) vbox = gtk.VBox() vbox.pack_start(repeat_songs_checkbutton, False, False, 0) hbox.pack_start(vbox, False, False, 0) vbox = gtk.VBox() vbox.pack_start(shuffle_songs_checkbutton, False, False, 0) hbox.pack_start(vbox, False, False, 0) top_bar_left_bottom.pack_start(hbox, False, False, 0) #top_bar_left_bottom.pack_start(gtk.Label("Volume: "), False, False, 0) #top_bar_left_bottom.pack_start(self.volume_slider, False, False, 2) #top_bar_left_bottom.pack_start(repeat_songs_checkbutton, False, False, 2) top_bar_left.pack_start(top_bar_left_top, False, False, 0) top_bar_left.pack_start(top_bar_left_bottom, False, False, 0) top_bar.pack_start(top_bar_left, False, False, 0) """End Top Control Bar""" ################################# # Scrubbing Bar ################################# vbox = gtk.VBox() vbox.pack_start(gtk.Label(" "), False, False, 1) # filler self.time_seek_label = gtk.Label(" ") vbox.pack_start(self.time_seek_label, False, False, 2) hbox = gtk.HBox() self.time_elapsed_label = gtk.Label("0:00") hbox.pack_start(self.time_elapsed_label, False, False, 2) self.time_elapsed_slider = gtk.HScale() self.time_elapsed_slider.set_inverted(False) self.time_elapsed_slider.set_range(0, 1) self.time_elapsed_slider.set_increments(1, 10) self.time_elapsed_slider.set_draw_value(False) self.time_elapsed_slider.set_update_policy(gtk.UPDATE_DELAYED) self.time_elapsed_signals = [] self.time_elapsed_signals.append(self.time_elapsed_slider.connect('value-changed', self.on_time_elapsed_slider_change)) self.time_elapsed_signals.append(self.time_elapsed_slider.connect('change-value', self.on_time_elapsed_slider_change_value)) hbox.pack_start(self.time_elapsed_slider, True, True, 2) self.time_total_label = gtk.Label("0:00") hbox.pack_start(self.time_total_label, False, False, 2) vbox.pack_start(hbox, False, False, 2) top_bar.pack_start(vbox) ################################# # Now Playing ################################# now_playing_info = gtk.VBox() filler = gtk.Label() self.current_song_label = gtk.Label() self.current_artist_label = gtk.Label() self.current_album_label = gtk.Label() now_playing_info.pack_start(filler, False, False, 0) now_playing_info.pack_start(self.current_song_label, False, False, 1) now_playing_info.pack_start(self.current_artist_label, False, False, 1) now_playing_info.pack_start(self.current_album_label, False, False, 1) top_bar.pack_start(now_playing_info, False, False, 5) ################################# # Album Art ################################# vbox = gtk.VBox() self.album_art_image = gtk.Image() event_box_album = gtk.EventBox() event_box_album.connect("button_release_event", self.button_album_art_clicked) event_box_album.add(self.album_art_image) hbox = gtk.HBox() ### Stars self.rating_stars_list = [] i = 0 while i < 5: # 5 stars self.rating_stars_list.append(gtk.Image()) hbox.pack_start(self.rating_stars_list[i], False, False, 1) i += 1 vbox.pack_start(event_box_album, False, False, 0) vbox.pack_start(hbox, False, False, 0) top_bar.pack_start(vbox, False, False, 1) ######## main_box.pack_start(top_bar, False, False, 3) """End Now Playing Info/Album Art""" ################################# # Middle Section ################################# hpaned = gtk.HPaned() hpaned.set_position(270) ################################# # Playlist / Downloads Window ################################# self.side_panel = gtk.VBox() ###################### Playlist ######################## self.playlist_window = gtk.VBox() playlist_scrolled_window = gtk.ScrolledWindow() playlist_scrolled_window.set_shadow_type(gtk.SHADOW_ETCHED_IN) playlist_scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) # str, title - artist - album, song_id self.playlist_list_store = gtk.ListStore(gtk.gdk.Pixbuf, str, int) tree_view = gtk.TreeView(self.playlist_list_store) self.tree_view_dict['playlist'] = tree_view tree_view.get_selection().set_mode(gtk.SELECTION_MULTIPLE) tree_view.set_reorderable(True) tree_view.connect("drag-end", self.on_playlist_drag) tree_view.connect("row-activated", self.playlist_on_activated) tree_view.connect("button_press_event", self.playlist_on_right_click) tree_view.set_rules_hint(True) new_column = guifunctions.create_column(" ", 0, None, True) new_column.set_reorderable(False) new_column.set_resizable(False) new_column.set_clickable(False) new_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) new_column.set_fixed_width(20) tree_view.append_column(new_column) renderer_text = gtk.CellRendererText() new_column = gtk.TreeViewColumn(_("Current Playlist"), renderer_text, markup=1) #new_column = guifunctions.create_column("Current Playlist", 1) new_column.set_reorderable(False) new_column.set_resizable(False) new_column.set_clickable(False) new_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) tree_view.append_column(new_column) playlist_scrolled_window.add(tree_view) self.playlist_window.pack_start(playlist_scrolled_window) hbox = gtk.HBox() button = gtk.Button(_("Clear Playlist")) button.connect('clicked', self.audio_engine.clear_playlist) hbox.pack_start(button, False, False, 2) combobox = gtk.combo_box_new_text() combobox.append_text(_('Replace Mode')) combobox.append_text(_('Add Mode')) combobox.connect('changed', self.playlist_mode_changed) hbox.pack_start(combobox, False, False, 2) self.playlist_window.pack_start(hbox, False, False, 2) self.side_panel.pack_start(self.playlist_window) ########################## Downloads ###################### self.downloads_window = gtk.VBox() downloads_panel_scrolled_window = gtk.ScrolledWindow() downloads_panel_scrolled_window.set_shadow_type(gtk.SHADOW_ETCHED_IN) downloads_panel_scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) downloads_window_list = gtk.VBox() self.downloads_list_store = gtk.ListStore(str, int, str) tree_view = gtk.TreeView(self.downloads_list_store) self.tree_view_dict['downloads'] = tree_view tree_view.connect("row-activated", self.downloads_on_activated) tree_view.connect("button_press_event", self.downloads_on_right_click) tree_view.set_rules_hint(True) column = gtk.TreeViewColumn(_("File"), gtk.CellRendererText(), text=0) column.set_reorderable(False) column.set_resizable(True) column.set_clickable(False) column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) column.set_fixed_width(100) tree_view.append_column(column) rendererprogress = gtk.CellRendererProgress() column = gtk.TreeViewColumn(_("Progress")) column.pack_start(rendererprogress, True) column.add_attribute(rendererprogress, "value", 1) column.set_reorderable(False) column.set_resizable(True) column.set_clickable(False) column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) tree_view.append_column(column) downloads_window_list.pack_start(tree_view) downloads_panel_scrolled_window.add_with_viewport(downloads_window_list) self.downloads_window.pack_start(downloads_panel_scrolled_window) self.side_panel.pack_start(self.downloads_window) ############################# hpaned.pack1(self.side_panel) #################################### # Artists/Albums/Songs #################################### middle_vpaned = gtk.VPaned() middle_vpaned.set_position(170) """Middle Top""" middle_top = gtk.HBox() """Middle Top Left""" ################################# # Artists ################################# artists_scrolled_window = gtk.ScrolledWindow() artists_scrolled_window.set_shadow_type(gtk.SHADOW_ETCHED_IN) artists_scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) renderer_text = gtk.CellRendererText() artists_column = gtk.TreeViewColumn(_("Artists"), renderer_text, markup=0) artists_column.set_resizable(False) artists_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) artists_column.set_sort_column_id(0) # name, id, custom_name self.artist_list_store = gtk.ListStore(str, int, str) self.artist_list_store.set_sort_func(0, helperfunctions.sort_artists_by_custom_name, artists_column) self.artist_list_store.set_sort_column_id(0, gtk.SORT_ASCENDING) tree_view = gtk.TreeView(self.artist_list_store) self.tree_view_dict['artists'] = tree_view tree_view.set_rules_hint(False) artists_column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) tree_view.append_column(artists_column) tree_view.connect("cursor-changed", self.artists_cursor_changed) #tree_view.connect("popup-menu", self.artists_cursor_changed) tree_view.set_search_column(0) artists_scrolled_window.add(tree_view) """End Middle Top Left""" """Begin Middle Top Right""" ################################# # Albums ################################# albums_scrolled_window = gtk.ScrolledWindow() albums_scrolled_window.set_shadow_type(gtk.SHADOW_ETCHED_IN) albums_scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) #albums_column = guifunctions.create_column("Albums", 0) renderer_text = gtk.CellRendererText() albums_column = gtk.TreeViewColumn(_("Albums"), renderer_text, markup=0) albums_column.set_resizable(False) albums_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) albums_column.set_sort_column_id(0) # name, id, year, stars self.album_list_store = gtk.ListStore(str, int, int, int) self.album_list_store.set_sort_column_id(0, gtk.SORT_ASCENDING) self.album_list_store.set_sort_func(0, helperfunctions.sort_albums_by_year, albums_column ) # sort albums by year! tree_view = gtk.TreeView(self.album_list_store) self.tree_view_dict['albums'] = tree_view tree_view.set_rules_hint(False) albums_column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) tree_view.append_column(albums_column) tree_view.connect("cursor-changed", self.albums_cursor_changed) tree_view.connect("row-activated", self.albums_on_activated) tree_view.connect("button_press_event", self.albums_on_right_click) tree_view.set_search_column(0) albums_scrolled_window.add(tree_view) """End Middle Top Right""" middle_top.pack_start(artists_scrolled_window, True, True, 0) middle_top.pack_start(albums_scrolled_window, True, True, 0) """End Middle Top""" """Middle Bottom""" ################################# # Songs ################################# songs_scrolled_window = gtk.ScrolledWindow() songs_scrolled_window.set_shadow_type(gtk.SHADOW_ETCHED_IN) songs_scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) # track, title, time, size, id self.song_list_store = gtk.ListStore(int, str, str, str, str, str, int) self.song_list_store.set_sort_func(0, helperfunctions.sort_songs_by_track) self.song_list_store.set_sort_func(1, helperfunctions.sort_songs_by_title) self.song_list_store.set_sort_func(2, helperfunctions.sort_songs_by_artist) self.song_list_store.set_sort_func(3, helperfunctions.sort_songs_by_album) self.song_list_store.set_sort_column_id(2,gtk.SORT_ASCENDING) tree_view = gtk.TreeView(self.song_list_store) self.tree_view_dict['songs'] = tree_view tree_view.get_selection().set_mode(gtk.SELECTION_MULTIPLE) tree_view.connect("row-activated", self.songs_on_activated) tree_view.connect("button_press_event", self.songs_on_right_click) tree_view.set_rules_hint(True) tree_view.set_search_column(1) i = 0 for column in (_("Track"), _("Title"), _("Artist"), _("Album"), _("Time"), _("Size")): new_column = guifunctions.create_column(column, i) new_column.set_reorderable(True) new_column.set_resizable(True) new_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) if column == _("Track"): new_column.set_fixed_width(65) elif column == _("Title"): new_column.set_fixed_width(230) elif column == _("Artist"): new_column.set_fixed_width(170) elif column == _("Album"): new_column.set_fixed_width(190) elif column == _("Time"): new_column.set_fixed_width(90) elif column == _("Size"): new_column.set_fixed_width(70) tree_view.append_column(new_column) i += 1 songs_scrolled_window.add(tree_view) """End Middle Bottom""" middle_vpaned.pack1(middle_top) middle_vpaned.pack2(songs_scrolled_window) hpaned.pack2(middle_vpaned) main_box.pack_start(hpaned, True, True, 0) """End Middle""" """Start status bar""" ################################# # Status Bar ################################# self.statusbar = gtk.Statusbar() self.statusbar.set_has_resize_grip(True) self.update_statusbar(_("Ready")) main_box.pack_start(self.statusbar, False, False, 0) """End status bar""" self.window.add(main_box) """Show All""" self.window.show_all() if view_statusbar == False: self.statusbar.hide() if show_playlist == False: self.playlist_window.hide() if show_downloads == False: self.downloads_window.hide() if show_downloads == False and show_playlist == False: self.side_panel.hide() """End Show All""" # check repeat songs if the user wants it repeat_songs = self.db_session.variable_get('repeat_songs', False) repeat_songs_checkbutton.set_active(repeat_songs) self.audio_engine.set_repeat_songs(repeat_songs) # check shuffle songs_on_activated shuffle_songs = self.db_session.variable_get('shuffle_songs', False) shuffle_songs_checkbutton.set_active(shuffle_songs) self.audio_engine.set_shuffle_songs(shuffle_songs) self.playlist_mode = self.db_session.variable_get('playlist_mode', 0) combobox.set_active(self.playlist_mode) if DBUS_AVAILABLE: try: session_bus = dbus.SessionBus() gnome_settings_daemon = session_bus.get_object("org.gnome.SettingsDaemon", "/org/gnome/SettingsDaemon/MediaKeys") media_keys = dbus.Interface(gnome_settings_daemon, "org.gnome.SettingsDaemon.MediaKeys") media_keys.connect_to_signal("MediaPlayerKeyPressed", self.media_key_pressed) except: pass def media_key_pressed(self, *args): """Support Media Key Presses -- Merged from Andrew Barr""" key = args[1] op_function = { "Previous" : self.button_prev_clicked, "Next" : self.button_next_clicked , "Stop" : self.audio_engine.stop , "Play" : self.button_play_pause_clicked } op_function[key]() def main_gui_callback(self): """Function that gets called after GUI has loaded. This loads all user variables into memory.""" ### Display Notifications ### self.display_notifications = self.db_session.variable_get('display_notifications', True) ### Automatically Update Cache ### self.automatically_update = self.db_session.variable_get('automatically_update', False) ### Is first time closing application (alert user it is in status bar) ### self.first_time_closing = self.db_session.variable_get('first_time_closing', True) ### Status tray variables ### self.quit_when_window_closed = self.db_session.variable_get('quit_when_window_closed', False) ### Downloads Directory ### # first check if xdg_downloads exists, if not then check if # ~/Downloads exist, then fallback to ~ (only if the user hasn't set one) download_dir = os.path.expanduser("~") if GLIB_AVAILABLE: # first try to set the Downloads dir to the users locale Download dir xdg_download_dir = glib.get_user_special_dir(glib.USER_DIRECTORY_DOWNLOAD) if not xdg_download_dir is None: # xdg was set download_dir = xdg_download_dir elif os.path.exists(os.path.join(download_dir, 'Downloads')): download_dir = os.path.join(download_dir, 'Downloads') else: # no glib.. try to set it to ~/Downloads if os.path.exists(os.path.join(download_dir, 'Downloads')): download_dir = os.path.join(download_dir, 'Downloads') self.downloads_directory = self.db_session.variable_get('downloads_directory', download_dir) ### Alternate Row Colors ### playlist = self.db_session.variable_get('playlist', True) downloads = self.db_session.variable_get('downloads', True) artists = self.db_session.variable_get('artists', False) albums = self.db_session.variable_get('albums', False) songs = self.db_session.variable_get('songs', True) self.tree_view_dict['playlist'].set_rules_hint(playlist) self.tree_view_dict['downloads'].set_rules_hint(downloads) self.tree_view_dict['artists'].set_rules_hint(artists) self.tree_view_dict['albums'].set_rules_hint(albums) self.tree_view_dict['songs'].set_rules_hint(songs) ### Check for credentials and login ### username = self.db_session.variable_get('credentials_username') password = self.db_session.variable_get('credentials_password') url = self.db_session.variable_get('credentials_url') self.ampache_conn.set_credentials(username, password, url) if self.ampache_conn.has_credentials(): self.go_to_ampache_menu_item.set_sensitive(True) self.update_statusbar(_("Attempting to authenticate...")) if self.login_and_get_artists("First"): list = self.db_session.variable_get('current_playlist', None) if list != None: self.load_playlist(list) #self.update_playlist_window() else: self.update_statusbar(_("Set Ampache information by going to Edit -> Preferences")) if self.is_first_time: self.create_dialog_alert("info", _("""This looks like the first time you are running Viridian. To get started, go to Edit -> Preferences and set your account information."""), True) if self.db_session.variable_get('enable_xmlrpc_server', False): # start the xmlrpc server self.start_xml_server() def main_gui_toggle_hidden(self): if self.window.is_active(): self.window.hide_on_delete() else: show_playlist = self.db_session.variable_get('show_playlist', True) show_downloads = self.db_session.variable_get('show_downloads', False) view_statusbar = self.db_session.variable_get('view_statusbar', True) self.window.show_all() self.window.grab_focus() self.window.present() if show_playlist == False: self.playlist_window.hide() if show_downloads == False: self.downloads_window.hide() if show_playlist == False and show_downloads == False: self.side_panel.hide() if view_statusbar == False: self.statusbar.hide() def show_settings(self, widget, data=None): """The settings pane""" ################################# # Settings Window ################################# if hasattr(self, 'preferences_window'): if self.preferences_window != None: self.preferences_window.present() return True self.preferences_window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.preferences_window.set_icon(self.images_pixbuf_viridian_simple) self.preferences_window.set_transient_for(self.window) self.preferences_window.set_title(_("Viridian Settings")) self.preferences_window.set_position(gtk.WIN_POS_CENTER_ON_PARENT) self.preferences_window.resize(450, 300) self.preferences_window.set_resizable(False) self.preferences_window.set_icon(self.images_pixbuf_viridian_simple) self.preferences_window.connect("delete_event", self.destroy_settings) self.preferences_window.connect("destroy", self.destroy_settings) main_vbox = gtk.VBox(False, 8) main_vbox.set_border_width(10) """Start Notebook""" notebook = gtk.Notebook() notebook.set_tab_pos(gtk.POS_LEFT) ################################# # Account Settings ################################# account_box = gtk.VBox() account_box.set_border_width(10) hbox = gtk.HBox() label = gtk.Label() label.set_markup(_('Account Settings')) hbox.pack_start(label, False, False) account_box.pack_start(hbox, False, False, 3) ### Ampache URL ### hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) ampache_label = gtk.Label(_("Ampache URL:")) hbox.pack_start(ampache_label, False, False, 2) self.ampache_text_entry = gtk.Entry() try: self.ampache_text_entry.set_text(self.ampache_conn.url) except: pass hbox.pack_start(self.ampache_text_entry) account_box.pack_start(hbox, False, False, 2) hbox = gtk.HBox() label = gtk.Label("") label.set_markup(_('Example: http://example.com/ampache')) hbox.pack_end(label, False, False, 2) account_box.pack_start(hbox, False, False, 2) ### Ampache Username ### hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) username_label = gtk.Label(_("Username:")) hbox.pack_start(username_label, False, False, 2) self.username_text_entry = gtk.Entry() try: self.username_text_entry.set_text(self.ampache_conn.username) except: pass hbox.pack_start(self.username_text_entry) account_box.pack_start(hbox, False, False, 2) ### Ampache Password ### hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) password_label = gtk.Label(_("Password:")) hbox.pack_start(password_label, False, False, 2) self.password_text_entry = gtk.Entry() try: self.password_text_entry.set_text(self.ampache_conn.password) except: pass self.password_text_entry.set_visibility(False) hbox.pack_start(self.password_text_entry) account_box.pack_start(hbox, False, False, 2) save = gtk.Button(stock=gtk.STOCK_SAVE) save.connect("clicked", self.button_save_preferences_clicked, self.preferences_window) hbox = gtk.HBox() hbox.pack_start(save, False, False, 4) account_box.pack_end(hbox, False, False, 2) """End Account Settings""" """Start Display Settings""" ################################# # display Settings ################################# display_box = gtk.VBox() display_box.set_border_width(10) hbox = gtk.HBox() label = gtk.Label() label.set_markup(_('Notifications')) hbox.pack_start(label, False, False) display_box.pack_start(hbox, False, False, 3) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) display_notifications_checkbutton = gtk.CheckButton(_("Display OSD Notifications")) if PYNOTIFY_INSTALLED: display_notifications_checkbutton.set_active(self.display_notifications) else: display_notifications_checkbutton.set_sensitive(False) display_notifications_checkbutton.set_active(False) display_notifications_checkbutton.connect("toggled", self.toggle_display_notifications) hbox.pack_start(display_notifications_checkbutton) display_box.pack_start(hbox, False, False, 1) hbox = gtk.HBox() label = gtk.Label() label.set_markup(_('Alternate Row Colors')) hbox.pack_start(label, False, False) display_box.pack_start(hbox, False, False, 3) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) cb = gtk.CheckButton(_("Artists Column")) cb.connect("toggled", self.toggle_alternate_row_colors, 'artists') cb.set_active(self.tree_view_dict['artists'].get_rules_hint()) hbox.pack_start(cb) display_box.pack_start(hbox, False, False, 0) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) cb = gtk.CheckButton(_("Albums Column")) cb.connect("toggled", self.toggle_alternate_row_colors, 'albums') cb.set_active(self.tree_view_dict['albums'].get_rules_hint()) hbox.pack_start(cb) display_box.pack_start(hbox, False, False, 0) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) cb = gtk.CheckButton(_("Songs Column")) cb.connect("toggled", self.toggle_alternate_row_colors, 'songs') cb.set_active(self.tree_view_dict['songs'].get_rules_hint()) hbox.pack_start(cb) display_box.pack_start(hbox, False, False, 0) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) cb = gtk.CheckButton(_("Playlist Column")) cb.connect("toggled", self.toggle_alternate_row_colors, 'playlist') cb.set_active(self.tree_view_dict['playlist'].get_rules_hint()) hbox.pack_start(cb) display_box.pack_start(hbox, False, False, 0) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) cb = gtk.CheckButton(_("Downloads Column")) cb.connect("toggled", self.toggle_alternate_row_colors, 'downloads') cb.set_active(self.tree_view_dict['downloads'].get_rules_hint()) hbox.pack_start(cb) display_box.pack_start(hbox, False, False, 0) """End Display Settings""" """Start Catalog Settings""" ################################# # catalog Settings ################################# catalog_box = gtk.VBox() catalog_box.set_border_width(10) hbox = gtk.HBox() label = gtk.Label() label.set_markup(_('Catalog Cache')) hbox.pack_start(label, False, False) catalog_box.pack_start(hbox, False, False, 3) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) cb = gtk.CheckButton(_("Automatically clear local catalog when Ampache is updated")) cb.set_active(self.automatically_update) cb.connect("toggled", self.toggle_automatically_update) hbox.pack_start(cb) catalog_box.pack_start(hbox, False, False, 2) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) label = gtk.Label() label.set_line_wrap(True) image = gtk.Image() if self.ampache_conn.has_credentials() and self.ampache_conn.is_authenticated(): if self.catalog_up_to_date: image.set_from_stock(gtk.STOCK_YES,gtk.ICON_SIZE_SMALL_TOOLBAR) label.set_text(_("Local catalog is up-to-date.")) else: image.set_from_stock(gtk.STOCK_NO,gtk.ICON_SIZE_SMALL_TOOLBAR) label.set_text(_("Local catalog is older than Ampache catalog! To update the local catalog go to File -> Clear Local Cache.")) hbox.pack_start(image, False, False, 5) hbox.pack_start(label, False, False, 0) catalog_box.pack_start(hbox, False, False, 2) """End Catalog Settings""" """Start Download Settings""" ################################# # Download Settings ################################# download_box = gtk.VBox(False, 0) download_box.set_border_width(10) hbox = gtk.HBox() label = gtk.Label() label.set_markup(_('Local Downloads')) hbox.pack_start(label, False, False) download_box.pack_start(hbox, False, False, 3) hbox = gtk.HBox() label = gtk.Label(_(" Select where downloaded files should go.")) hbox.pack_start(label, False, False, 4) download_box.pack_start(hbox, False, False, 2) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 1) self.downloads_text_entry = gtk.Entry() self.downloads_text_entry.set_text(self.downloads_directory) hbox.pack_start(self.downloads_text_entry) fcbutton = gtk.Button(stock=gtk.STOCK_OPEN) fcbutton.connect('clicked', self.button_open_downloads_file_chooser_clicked) hbox.pack_start(fcbutton, False, False, 4) download_box.pack_start(hbox, False, False, 2) """End Download Settings""" """Start Tray Icon Settings""" ################################# # Tray Icon Settings ################################# trayicon_box = gtk.VBox(False, 0) trayicon_box.set_border_width(10) hbox = gtk.HBox() label = gtk.Label() label.set_markup(_('Status Tray Icon')) hbox.pack_start(label, False, False) trayicon_box.pack_start(hbox, False, False, 3) cb = gtk.CheckButton(_("Quit Viridian when window is closed")) cb.connect("toggled", self.toggle_quit_when_window_closed) cb.set_active(self.quit_when_window_closed) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) button = gtk.RadioButton(None, _("Standard Tray Icon")) button.connect("toggled", self.trayicon_settings_toggled, "standard", cb) if self.tray_icon_to_display == 'standard': button.set_active(True) hbox.pack_start(button, True, True, 0) trayicon_box.pack_start(hbox, False, False, 2) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) button = gtk.RadioButton(button, _("Unified Sound Icon ( Ubuntu 10.10 or higher )")) button.connect("toggled", self.trayicon_settings_toggled, "unified", cb) button.set_sensitive(False) # Ubuntu unified sound if self.tray_icon_to_display == 'unified': button.set_active(True) hbox.pack_start(button, True, True, 0) trayicon_box.pack_start(hbox, False, False, 2) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) button = gtk.RadioButton(button, _("Disabled")) button.connect("toggled", self.trayicon_settings_toggled, "disabled", cb) if self.tray_icon_to_display == 'disabled': button.set_active(True) hbox.pack_start(button, True, True, 0) trayicon_box.pack_start(hbox, False, False, 2) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) hbox.pack_start(cb, True, True, 0) trayicon_box.pack_start(hbox, False, False, 5) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) label = gtk.Label(_("Note: changes to the type of icon will take effect the next time this program is opened.")) label.set_line_wrap(True) hbox.pack_start(label, False, False, 4) trayicon_box.pack_start(hbox, False, False, 5) """End Tray Icon Settings""" """Start Server Settings""" ################################# # Server Settings ################################# server_box = gtk.VBox(False, 0) server_box.set_border_width(10) hbox = gtk.HBox() label = gtk.Label() label.set_markup(_('Server Settings')) hbox.pack_start(label, False, False) server_box.pack_start(hbox, False, False, 3) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) hbox.pack_start(gtk.Label(_("XML RPC Server: ")), False, False, 0) label = gtk.Label() image = gtk.Image() if self.xml_server.is_running: image.set_from_stock(gtk.STOCK_YES,gtk.ICON_SIZE_SMALL_TOOLBAR) label.set_text(_("Running. (port %d)") % self.xml_server.port) else: image.set_from_stock(gtk.STOCK_NO,gtk.ICON_SIZE_SMALL_TOOLBAR) label.set_text(_("Not Running.")) hbox.pack_start(image, False, False, 5) hbox.pack_start(label, False, False, 0) server_box.pack_start(hbox, False, False, 2) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) port = gtk.Entry() button = gtk.Button(_("Start")) button.connect("clicked", self.button_xml_server_clicked, 'start', label, image, port) #button.set_sensitive(False) hbox.pack_start(button, True, True, 0) button = gtk.Button(_("Stop")) button.connect("clicked", self.button_xml_server_clicked, 'stop', label, image, port) #button.set_sensitive(False) hbox.pack_start(button, True, True, 0) button = gtk.Button(_("Restart")) button.connect("clicked", self.button_xml_server_clicked, 'restart', label, image, port) #button.set_sensitive(False) hbox.pack_start(button, True, True, 0) hbox.pack_start(gtk.Label(_('Port: ')), False, False, 1) port.set_text(str(self.db_session.variable_get('xmlrpc_port', XML_RPC_PORT))) hbox.pack_start(port) server_box.pack_start(hbox, False, False, 2) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) cb = gtk.CheckButton(_("Start XML RPC server when Viridan starts")) cb.connect("toggled", self.toggle_start_xml_rpc_server) start_xml_rpc_server = self.db_session.variable_get('enable_xmlrpc_server', False) cb.set_active(start_xml_rpc_server) hbox.pack_start(cb, False, False, 1) server_box.pack_start(hbox, False, False, 2) """End Server Settings""" """Start System Settings""" ################################# # System Settings ################################# system_box = gtk.VBox() system_box.set_border_width(10) hbox = gtk.HBox() label = gtk.Label() label.set_markup(_('System')) hbox.pack_start(label, False, False) system_box.pack_start(hbox, False, False, 3) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) label = gtk.Label(_("To delete all personal information (including your username, password, album-art, cached information, etc.) press this button. NOTE: This will delete all personal settings stored on this computer and Viridian will close itself. When you reopen, it will be as though it is the first time you are running Viridian.")) label.set_line_wrap(True) hbox.pack_start(label, False, False) system_box.pack_start(hbox, False, False, 2) hbox = gtk.HBox() hbox.pack_start(gtk.Label(" "), False, False, 0) cb = gtk.Button(_("Reset Everything")) cb.connect("clicked", self.button_reset_everything_clicked) hbox.pack_start(cb, False, False, 2) system_box.pack_start(hbox, False, False, 2) """End System Settings""" """End Notebook""" notebook.append_page(account_box, gtk.Label(_("Account"))) notebook.append_page(display_box, gtk.Label(_("Display"))) notebook.append_page(catalog_box, gtk.Label(_("Catalog"))) notebook.append_page(download_box, gtk.Label(_("Downloads"))) notebook.append_page(trayicon_box, gtk.Label(_("Tray Icon"))) notebook.append_page(server_box, gtk.Label(_("Server"))) notebook.append_page(system_box, gtk.Label(_("System"))) """Start Bottom Bar""" bottom_bar = gtk.HBox() close = gtk.Button(stock=gtk.STOCK_CLOSE) close.connect("clicked", self.button_cancel_preferences_clicked, self.preferences_window) bottom_bar.pack_end(close, False, False, 2) """End Bottom Bar""" main_vbox.pack_start(notebook) main_vbox.pack_start(bottom_bar, False, False, 2) """End bottom row""" self.preferences_window.add(main_vbox) self.preferences_window.show_all() def destroy_settings(self, widget=None, data=None): """Close the preferences window.""" self.preferences_window.destroy() self.preferences_window = None def show_help(self, widget, data=None): """The Help pane""" ################################# # Help Window ################################# if hasattr(self, 'help_window'): if self.help_window != None: self.help_window.present() return True self.help_window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.help_window.set_transient_for(self.window) self.help_window.set_title(_("Viridian Help")) self.help_window.set_icon(self.images_pixbuf_viridian_simple) self.help_window.set_position(gtk.WIN_POS_CENTER_ON_PARENT) self.help_window.resize(350, 300) self.help_window.set_resizable(False) self.help_window.connect("delete_event", self.destroy_help) self.help_window.connect("destroy", self.destroy_help) vbox = gtk.VBox(False, 4) vbox.set_border_width(10) label = gtk.Label() label.set_markup(_('Viridian Help')) vbox.pack_start(label, False, False, 1) hbox = gtk.HBox() label = gtk.Label(_("Home Page:")) link = guifunctions.hyperlink('http://viridian.daveeddy.com') hbox.pack_start(label, False, False, 1) hbox.pack_start(link, False, False, 2) vbox.pack_start(hbox, False, False, 0) hbox = gtk.HBox() label = gtk.Label(_("Launchpad:")) link = guifunctions.hyperlink('https://launchpad.net/viridianplayer') hbox.pack_start(label, False, False, 1) hbox.pack_start(link, False, False, 2) vbox.pack_start(hbox, False, False, 0) hbox = gtk.HBox() label = gtk.Label(_("FAQ:")) link = guifunctions.hyperlink('https://answers.launchpad.net/viridianplayer/+faqs') hbox.pack_start(label, False, False, 1) hbox.pack_start(link, False, False, 2) vbox.pack_start(hbox, False, False, 0) hbox = gtk.HBox() label = gtk.Label(_("Bugs:")) link = guifunctions.hyperlink('https://bugs.launchpad.net/viridianplayer') hbox.pack_start(label, False, False, 1) hbox.pack_start(link, False, False, 2) vbox.pack_start(hbox, False, False, 0) hbox = gtk.HBox() label = gtk.Label(_("Questions:")) link = guifunctions.hyperlink('https://answers.launchpad.net/viridianplayer') hbox.pack_start(label, False, False, 1) hbox.pack_start(link, False, False, 2) vbox.pack_start(hbox, False, False, 0) self.help_window.add(vbox) self.help_window.show_all() def destroy_help(self, widget=None, data=None): self.help_window.destroy() self.help_window = None def show_playlist_select(self, type=None): """The playlist pane""" ################################# # playlist select ################################# if type != "Load" and type != "Save" and type != "Export": return True if hasattr(self, 'playlist_select_window'): if self.playlist_select_window != None: self.playlist_select_window.present() return True self.playlist_select_window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.playlist_select_window.set_transient_for(self.window) self.playlist_select_window.set_position(gtk.WIN_POS_CENTER_ON_PARENT) self.playlist_select_window.resize(490, 300) self.playlist_select_window.set_resizable(True) self.playlist_select_window.set_icon(self.images_pixbuf_viridian_simple) self.playlist_select_window.connect("delete_event", self.destroy_playlist) self.playlist_select_window.connect("destroy", self.destroy_playlist) self.playlist_select_window.set_title(type + " playlist") vbox = gtk.VBox() vbox.set_border_width(10) hbox = gtk.HBox() hbox.pack_start(gtk.Label("Select a Playlist to " + type + "..."), False, False, 2) vbox.pack_start(hbox, False, False, 2) scrolled_window = gtk.ScrolledWindow() scrolled_window.set_shadow_type(gtk.SHADOW_ETCHED_IN) scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) # name, items, owner, type, id playlist_list_store = gtk.ListStore(str, int, str, str, int) tree_view = gtk.TreeView(playlist_list_store) tree_view.set_rules_hint(True) i = 0 for column in (_("Name"), _("Songs"), _("Owner"), _("Type")): if column == _("Name"): renderer_text = gtk.CellRendererText() new_column = gtk.TreeViewColumn(column, renderer_text, markup=0) else: new_column = guifunctions.create_column(column, i) new_column.set_reorderable(True) new_column.set_resizable(True) new_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) if column == _("Name"): new_column.set_fixed_width(200) elif column == _("Songs"): new_column.set_fixed_width(70) elif column == _("Owner"): new_column.set_fixed_width(90) elif column == _("Type"): new_column.set_fixed_width(60) tree_view.append_column(new_column) i += 1 scrolled_window.add(tree_view) vbox.pack_start(scrolled_window, True, True, 5) text_entry = gtk.Entry() text_entry.set_text('') if type == 'Save': hbox = gtk.HBox() hbox.pack_start(gtk.Label(_("Playlist Name: ")), False, False, 1) hbox.pack_start(text_entry, False, False, 2) vbox.pack_start(hbox, False, False, 2) bottom_bar = gtk.HBox() remove = gtk.Button(stock=gtk.STOCK_DELETE) remove.connect("clicked", self.button_delete_playlist_clicked, tree_view.get_selection(), type) close = gtk.Button(stock=gtk.STOCK_CLOSE) close.connect("clicked", self.destroy_playlist) button = gtk.Button(stock=gtk.STOCK_SAVE) if type == 'Load': button = gtk.Button(stock=gtk.STOCK_OPEN) elif type == 'Export': button = gtk.Button(_("Export as M3U...")) button.connect("clicked", self.button_load_or_save_playlist_clicked, tree_view.get_selection(), text_entry, type) bottom_bar.pack_start(remove, False, False, 2) bottom_bar.pack_end(button, False, False, 2) bottom_bar.pack_end(close, False, False, 2) vbox.pack_start(bottom_bar, False, False, 1) self.playlist_select_window.add(vbox) self.update_playlist_select(type, playlist_list_store) self.playlist_select_window.show_all() def update_playlist_select(self, type, playlist_list_store): """Refresh the contents of the playlist list store""" playlist_list_store.clear() if type == "Load": # load playlists window # get playlists from Ampache ampache_playlists = self.ampache_conn.get_playlists() if ampache_playlists == None: ampache_playlists = [] print ampache_playlists playlist_list_store.append([' - Ampache Playlists - ', len(ampache_playlists), '----', '----', -1]) if len(ampache_playlists) == 0: playlist_list_store.append(['-(None)-', 0, '', '', -1]) else: for playlist in ampache_playlists: playlist_list_store.append([ helperfunctions.convert_string_to_html(playlist['name']), playlist['items'], playlist['owner'], playlist['type'], playlist['id']]) # get playlists stored locally local_playlists = dbfunctions.get_playlists(self.db_session) playlist_list_store.append([' - Local Playlists - ', len(local_playlists), '----', '----', -1]) if len(local_playlists) == 0: playlist_list_store.append(['-(None)-', 0, '', '', -1]) else: for playlist in local_playlists: playlist_list_store.append([ helperfunctions.convert_string_to_html(playlist['name']), len(playlist['songs']), getpass.getuser(), 'Local', -2]) def destroy_playlist(self, widget=None, data=None): """Close the playlist window.""" self.playlist_select_window.destroy() self.playlist_select_window = None def show_plugins_window(self, *args): """The Plugins Window""" ################################# # The Plugins Window ################################# if hasattr(self, 'plugins_window'): if self.plugins_window != None: self.plugins_window.present() return True self.plugins_window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.plugins_window.set_transient_for(self.window) self.plugins_window.set_position(gtk.WIN_POS_CENTER_ON_PARENT) self.plugins_window.resize(490, 300) self.plugins_window.set_resizable(True) self.plugins_window.set_icon(self.images_pixbuf_viridian_simple) self.plugins_window.connect("delete_event", self.destroy_plugins_window) self.plugins_window.connect("destroy", self.destroy_plugins_window) self.plugins_window.set_title(_("Viridian Plugins")) vbox = gtk.VBox() vbox.set_border_width(10) hbox = gtk.HBox() hbox.pack_start(gtk.Label(_("Select a plugin.")), False, False, 2) vbox.pack_start(hbox, False, False, 2) scrolled_window = gtk.ScrolledWindow() scrolled_window.set_shadow_type(gtk.SHADOW_ETCHED_IN) scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) list_store = gtk.ListStore(gobject.TYPE_BOOLEAN, str, str, str) treeview = gtk.TreeView(list_store) bool_cell_renderer = gtk.CellRendererToggle() bool_cell_renderer.set_property('activatable', True) bool_cell_renderer.connect('toggled', self.toggle_plugin, list_store) bool_col = gtk.TreeViewColumn(_("Enabled"), bool_cell_renderer, active=0) treeview.append_column(bool_col) renderer_text = gtk.CellRendererText() name_column = gtk.TreeViewColumn(_("Name"), renderer_text, text=1) treeview.append_column(name_column) renderer_text = gtk.CellRendererText() desc_column = gtk.TreeViewColumn(_("Description"), renderer_text, text=2) treeview.append_column(desc_column) # Check for new plugins since launch self.plugins = {} new_plugins_list = self.__find_plugins(PLUGINS_DIR) for plugin in new_plugins_list: imported_plugin = self.__import_plugin(plugin) if imported_plugin != None: self.plugins[plugin] = imported_plugin for name, plugin in self.plugins.iteritems(): enabled = False if name in self.enabled_plugins: enabled = True try: title = plugin.title description = plugin.description author = plugin.author list_store.append((enabled, title, description, name)) except: print _("Error: plugin '%s' could not be loaded because it is missing a title, description, and author instance variable") % (name) scrolled_window.add(treeview) vbox.pack_start(scrolled_window, True, True, 5) bottom_bar = gtk.HBox() close = gtk.Button(stock=gtk.STOCK_CLOSE) close.connect("clicked", self.destroy_plugins_window) bottom_bar.pack_end(close, False, False, 2) vbox.pack_start(bottom_bar, False, False, 1) self.plugins_window.add(vbox) self.plugins_window.show_all() def destroy_plugins_window(self, *args): """Close the playlist window.""" self.plugins_window.destroy() self.plugins_window = None ####################################### # Status Icon ####################################### def status_icon_activate(self, icon=None): """Bring the window back up when the user clicks the sys tray icon.""" self.main_gui_toggle_hidden() def status_icon_popup_menu(self, icon, button, activate_time): """Create a menu when the user right clicks the sys tray icon.""" menu = gtk.Menu() show_window = gtk.MenuItem(_("Show Window")) show_window.connect('activate', self.status_icon_activate) menu.append(show_window) ### Display Song Info is song is playing ### if self.audio_engine.get_state() != "stopped" and self.audio_engine.get_state() != None: menu.append(gtk.SeparatorMenuItem()) np = gtk.MenuItem(_("- Now Playing -")) if self.audio_engine.get_state() == "paused": np = gtk.MenuItem(_("- Now Playing (paused) -")) np.set_sensitive(False) menu.append(np) title = gtk.MenuItem(self.current_song_info['song_title']) artist = gtk.MenuItem(self.current_song_info['artist_name']) album = gtk.MenuItem(self.current_song_info['album_name']) title.set_sensitive(False) artist.set_sensitive(False) album.set_sensitive(False) menu.append(title) menu.append(artist) menu.append(album) menu.append(gtk.SeparatorMenuItem()) prev_track = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PREVIOUS) prev_track.connect('activate', self.button_prev_clicked, None) menu.append(prev_track) play_pause = gtk.MenuItem("") if self.audio_engine.get_state() != "playing": play_pause = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY) else: play_pause = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PAUSE) play_pause.connect('activate', self.button_play_pause_clicked, None) menu.append(play_pause) next_track = gtk.ImageMenuItem(gtk.STOCK_MEDIA_NEXT) next_track.connect('activate', self.button_next_clicked, None) menu.append(next_track) menu.append(gtk.SeparatorMenuItem()) pref = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES) pref.connect('activate', self.show_settings, None) menu.append(pref) quit_ = gtk.ImageMenuItem(gtk.STOCK_QUIT) quit_.connect('activate', self.destroy, None) menu.append(quit_) menu.show_all() menu.popup(None, None, gtk.status_icon_position_menu, button, activate_time, icon) ####################################### # Check Boxes ####################################### def toggle_statusbar_view(self, widget, data=None): """Toggle the status bar to show or hide it.""" if widget.active: self.statusbar.show() else: self.statusbar.hide() self.db_session.variable_set('view_statusbar', widget.active) def toggle_playlist_view(self, widget, data=None): """Toggle the playlist window.""" if widget.active: if self.downloads_window.flags() & gtk.VISIBLE == False: self.side_panel.show() self.playlist_window.show() else: if self.downloads_window.flags() & gtk.VISIBLE== False: self.side_panel.hide() self.playlist_window.hide() self.db_session.variable_set('show_playlist', widget.active) def toggle_downloads_view(self, widget, data=None): """Toggle the Downloads window.""" if widget.active: if self.playlist_window.flags() & gtk.VISIBLE == False: self.side_panel.show() self.downloads_window.show() else: if self.playlist_window.flags() & gtk.VISIBLE == False: self.side_panel.hide() self.downloads_window.hide() self.db_session.variable_set('show_downloads', widget.active) def toggle_repeat_songs(self, widget, data=None): """Toggle repeat songs.""" self.audio_engine.set_repeat_songs(widget.get_active()) self.db_session.variable_set('repeat_songs', widget.get_active()) def toggle_shuffle_songs(self, widget, data=None): """Toggle repeat songs.""" self.audio_engine.set_shuffle_songs(widget.get_active()) self.db_session.variable_set('shuffle_songs', widget.get_active()) def toggle_display_notifications(self, widget, data=None): """Toggle displaying notify OSD notifications.""" self.display_notifications = widget.get_active() self.db_session.variable_set('display_notifications', widget.get_active()) def toggle_automatically_update(self, widget, data=None): """Toggle automatically updating the local cache.""" self.automatically_update = widget.get_active() self.db_session.variable_set('automatically_update', widget.get_active()) def toggle_alternate_row_colors(self, widget, data=None): """Toggle set rulse hint for the given treeview column.""" self.tree_view_dict[data].set_rules_hint(widget.get_active()) self.db_session.variable_set(data, widget.get_active()) def toggle_quit_when_window_closed(self, widget, data=None): """Toggle to decide if the program quits or keeps running when the main window is closed.""" self.quit_when_window_closed = widget.get_active() self.db_session.variable_set('quit_when_window_closed', widget.get_active()) def toggle_start_xml_rpc_server(self, widget, data=None): """Toggle whether to start the XML server when Viridian opes.""" self.db_session.variable_set('enable_xmlrpc_server', widget.get_active()) def toggle_plugin(self, widget, path, model): """Toggle a plugin being active""" plugin_name = model[path][3] going_to = not model[path][0] if going_to == True: self.enabled_plugins.append(plugin_name) else: self.enabled_plugins.remove(plugin_name) # save the new list self.db_session.variable_set('enabled_plugins', self.enabled_plugins) # this line will tick or untick the checkmark model[path][0] = not model[path][0] ####################################### # Radio Buttons ####################################### def trayicon_settings_toggled(self, widget, name=None, cb=None): if widget.get_active(): if name == "disabled": self.quit_when_window_closed = True self.db_session.variable_set('quit_when_window_closed', self.quit_when_window_closed) self.tray_icon_to_display = 'disabled' self.db_session.variable_set('tray_icon_to_display', self.tray_icon_to_display) cb.set_active(True) cb.set_sensitive(False) elif name == "unified": self.quit_when_window_closed = False self.db_session.variable_set('quit_when_window_closed', self.quit_when_window_closed) self.tray_icon_to_display = 'unified' self.db_session.variable_set('tray_icon_to_display', self.tray_icon_to_display) cb.set_sensitive(True) cb.set_active(False) elif name == "standard": self.quit_when_window_closed = False self.db_session.variable_set('quit_when_window_closed', self.quit_when_window_closed) self.tray_icon_to_display = 'standard' self.db_session.variable_set('tray_icon_to_display', self.tray_icon_to_display) cb.set_sensitive(True) cb.set_active(False) ####################################### # Combo Boxes ####################################### def playlist_mode_changed(self, combobox): model = combobox.get_model() index = combobox.get_active() self.playlist_mode = index self.db_session.variable_set('playlist_mode', self.playlist_mode) return True ####################################### # Initial Authentication ####################################### def login_and_get_artists(self, data=None): """Authenticate and populate the artists.""" self.stop_all_threads() self.__clear_all_list_stores() self.update_statusbar(_("Authenticating...")) # get the user inforamiton ampache = self.ampache_conn.url username = self.ampache_conn.username password = self.ampache_conn.password print "--- Attempting to login to Ampache ---" print "Ampache = %s" % ampache print "Username = %s" % username print "Password = " + len(password)*"*" # set the credentials and try to login self.__successfully_authed = None ################ Thread to authenticate (so the GUI doesn't lock) ############ thread.start_new_thread(self.__authenticate, (None,)) while self.__successfully_authed == None: self.refresh_gui() self.go_to_ampache_menu_item.set_sensitive(True) ############################################################################## if self.__successfully_authed == True: # auth successful self.update_statusbar("Authentication Successful.") print "Authentication Successful!" print "Authentication = %s" % self.ampache_conn.auth print "Number of artists = %d" % self.ampache_conn.artists_num db_time = int(self.db_session.variable_get('catalog_update', -1)) ampache_time = int(self.ampache_conn.get_last_update_time()) if data == "changed": db_time = ampache_time self.db_session.variable_set('catalog_update', db_time) self.catalog_up_to_date = False if db_time >= ampache_time and ampache_time != -1: self.catalog_up_to_date = True if not self.catalog_up_to_date: # not up to date if data == "First" and self.automatically_update: # first time opening, update auto dbfunctions.clear_cached_catalog(self.db_session) self.db_session.variable_set('catalog_update', ampache_time) self.catalog_up_to_date = True elif data == True or data == "First": # open a popup if self.create_catalog_updated_dialog(): # user pressed update dbfunctions.clear_cached_catalog(self.db_session) self.db_session.variable_set('catalog_update', ampache_time) self.catalog_up_to_date = True elif data == None: # clear_cache_button pressed self.db_session.variable_set('catalog_update', ampache_time) self.catalog_up_to_date = True #else: #do nothing, pull from cache # load the artists window with, you guessed it, artists self.update_statusbar(_("Pulling Artists...")) self.check_and_populate_artists() artists = dbfunctions.get_artist_dict(self.db_session) model = self.artist_list_store model.append([_("All Artists (%d)" % len(artists)), -1, '']) for artist_id in artists: artist_name = artists[artist_id]['name'] custom_name = artists[artist_id]['custom_name'] model.append([helperfunctions.convert_string_to_html(artist_name), artist_id, custom_name]) # now pull all the albums.. this should cut down on precache time self.update_statusbar(_("Pulling Albums...")) self.check_and_populate_albums() self.albums = dbfunctions.get_album_dict(self.db_session) self.update_statusbar(_("Ready.")) return True else: # auth failed error = self.__successfully_authed if error == None or error == False: error = _("Unknown error, possibly an incorrect URL specified, or the server is not responding.") self.update_statusbar(_("Authentication Failed.")) self.create_dialog_alert("error", _("Error Authenticating\n\n") + error, True) return False ####################################### # Selection Methods (Single Click) ####################################### def artists_cursor_changed(self, widget, data=None): """The function that runs when the user clicks an artist.""" cursor = widget.get_cursor() model = widget.get_model() row = cursor[0] artist_name = model[row][0] artist_id = model[row][1] try: if self.artist_id == artist_id and self.artist_name == artist_name: return True # don't refresh if the user reclicks the artist except: pass self.artist_id = artist_id self.artist_name = artist_name self.album_id = None # this is the albums refresh # now display the albums model = self.album_list_store model.clear() self.check_and_populate_albums(self.artist_id) if self.artist_id == -1: # all artists albums = self.albums else: albums = dbfunctions.get_album_dict(self.db_session, self.artist_id) self.update_statusbar("Loading: " + self.artist_name.replace('', '').replace('', '')) model.append([_("All Albums (%d)") % (len(albums)), -1, -1, 0]) for album in albums: album_name = albums[album]['name'] album_year = albums[album]['year'] precise_rating = albums[album]['precise_rating'] album_id = album #self.update_statusbar(_("Fetching Album: ") + album_name) album_string = album_name + ' (' + str(album_year) + ')' if album_year == 0: album_string = album_name model.append([helperfunctions.convert_string_to_html(album_string), album_id, album_year, precise_rating]) self.update_statusbar(self.artist_name.replace('', '').replace('', '')) def albums_cursor_changed(self, widget, data=None): """The function that runs when the user clicks an album.""" cursor = widget.get_cursor() model = widget.get_model() row = cursor[0] album_name = model[row][0] album_id = model[row][1] try: if self.album_id == album_id and self.album_name == album_name: return True # don't refresh if the user reclicks the album except: pass self.continue_load_songs = False self.album_name = album_name self.album_id = album_id song_list_store = self.song_list_store song_list_store.clear() if album_id == -1: # all albums self.continue_load_songs = True list = [] for album in model: list.append(album[1]) for album_id in list: if album_id != -1: if self.continue_load_songs == False: return False if self.__add_songs_to_list_store(album_id): self.update_statusbar(_("Fetching Album id: ") + str(album_id)) self.update_statusbar(album_name.replace('', '').replace('', '') + " - " + self.artist_name.replace('', '').replace('', '')) else: # single album if self.__add_songs_to_list_store(album_id): self.update_statusbar(album_name + " - " + self.artist_name.replace('', '').replace('', '')) ####################################### # Selection Methods (Double Click) ####################################### def albums_on_activated(self, widget, row, col): """The function that runs when the user double-clicks an album.""" model = widget.get_model() album_name = model[row][0] album_id = model[row][1] if self.playlist_mode == 0: # replace mode # get all songs in the current songs menu and play them list = [] for song in self.song_list_store: list.append(song[6]) print "Sending this list of songs to player", list self.audio_engine.play_from_list_of_songs(list) else: # add mode for song in self.song_list_store: self.audio_engine.insert_into_playlist(song[6]) self.update_playlist_window() def songs_on_activated(self, widget, row, col): """The function that runs when the user double-clicks a song.""" model = widget.get_model() song_title = model[row][1] song_id = model[row][6] if self.playlist_mode == 0: # replace mode list = [] for song in model: list.append(song[6]) song_num = row[0] print "Sending this list of songs to player", list self.audio_engine.play_from_list_of_songs(list, song_num) else: # add mode self.audio_engine.insert_into_playlist(song_id) self.update_playlist_window() def playlist_on_activated(self, widget, row, col): """The function that runs when the user double-clicks a song in the playlist.""" song_num = row[0] self.audio_engine.change_song(song_num) def downloads_on_activated(self, widget, row, col): """The function that runs when the user double-clicks a song in the downloads window.""" model = widget.get_model() full_path = model[row][2] self.gnome_open(os.path.dirname(full_path)) ####################################### # Selection Methods (right-click) ####################################### def foreach(self, model, path, iter, data): """Helper for multi-select.""" list = data[0] column = data[1] list.append(model.get_value(iter, column)) def playlist_on_right_click(self, treeview, event, data=None): """The user right-clicked the playlist.""" if event.button == 3: # check to see if there is multiple selections list = [] self.tree_view_dict['playlist'].get_selection().selected_foreach(self.foreach, [list, 2]) x = int(event.x) y = int(event.y) pthinfo = treeview.get_path_at_pos(x, y) if len(list) > 1: # multiple selected if pthinfo != None: path, col, cellx, celly = pthinfo # create popup song_id = treeview.get_model()[path][2] m = gtk.Menu() i = gtk.MenuItem(_("Remove From Playlist")) i.connect('activate', self.remove_from_playlist, song_id, treeview, list) m.append(i) m.append(gtk.SeparatorMenuItem()) i = gtk.MenuItem(_("Download Songs")) i.connect('activate', self.download_songs_clicked, list) m.append(i) m.show_all() m.popup(None, None, None, event.button, event.time, None) return True else: # single selected if pthinfo != None: path, col, cellx, celly = pthinfo # create popup song_id = treeview.get_model()[path][2] m = gtk.Menu() i = gtk.MenuItem(_("Remove From Playlist")) i.connect('activate', self.remove_from_playlist, song_id, treeview) m.append(i) m.append(gtk.SeparatorMenuItem()) i = gtk.MenuItem(_("Download Song")) i.connect('activate', self.download_song_clicked, song_id) m.append(i) i = gtk.MenuItem(_("Copy URL to Clipboard")) i.connect('activate', lambda y: self.copy_song_url_to_clipboard(song_id)) m.append(i) m.show_all() m.popup(None, None, None, event.button, event.time, None) def downloads_on_right_click(self, treeview, event, data=None): if event.button == 3: x = int(event.x) y = int(event.y) pthinfo = treeview.get_path_at_pos(x, y) if pthinfo != None: path, col, cellx, celly = pthinfo # create popup full_path = treeview.get_model()[path][2] m = gtk.Menu() i = gtk.MenuItem(_("Open Song")) i.connect('activate', lambda _: self.gnome_open(full_path)) m.append(i) i = gtk.MenuItem(_("Open Containing Folder")) i.connect('activate', lambda _: self.gnome_open(os.path.dirname(full_path))) m.append(i) m.show_all() m.popup(None, None, None, event.button, event.time, None) def albums_on_right_click(self, treeview, event, data=None): if event.button == 3: x = int(event.x) y = int(event.y) pthinfo = treeview.get_path_at_pos(x, y) if pthinfo != None: path, col, cellx, celly = pthinfo # create popup album_id = treeview.get_model()[path][1] m = gtk.Menu() i = gtk.MenuItem(_("Add Album to Playlist")) i.connect('activate', self.add_album_to_playlist) m.append(i) m.append(gtk.SeparatorMenuItem()) i = gtk.MenuItem(_("Download Album")) i.connect('activate', self.download_album_clicked) m.append(i) m.show_all() m.popup(None, None, None, event.button, event.time, None) def songs_on_right_click(self, treeview, event, data=None): if event.button == 3: x = int(event.x) y = int(event.y) pthinfo = treeview.get_path_at_pos(x, y) # check to see if there is multiple selections list = [] self.tree_view_dict['songs'].get_selection().selected_foreach(self.foreach, [list, 6]) if len(list) > 1: # multiple selected if pthinfo != None: path, col, cellx, celly = pthinfo # create popup m = gtk.Menu() i = gtk.MenuItem(_("Add Songs to Playlist")) i.connect('activate', self.add_songs_to_playlist, list) m.append(i) m.append(gtk.SeparatorMenuItem()) i = gtk.MenuItem(_("Download Songs")) i.connect('activate', self.download_songs_clicked, list) m.append(i) m.show_all() m.popup(None, None, None, event.button, event.time, None) return True else: # single song if pthinfo != None: path, col, cellx, celly = pthinfo # create popup song_id = treeview.get_model()[path][6] m = gtk.Menu() i = gtk.MenuItem(_("Add Song to Playlist")) i.connect('activate', self.add_song_to_playlist, song_id) m.append(i) m.append(gtk.SeparatorMenuItem()) i = gtk.MenuItem(_("Download Song")) i.connect('activate', self.download_song_clicked, song_id) m.append(i) i = gtk.MenuItem(_("Copy URL to Clipboard")) i.connect('activate', lambda y: self.copy_song_url_to_clipboard(song_id)) m.append(i) m.show_all() m.popup(None, None, None, event.button, event.time, None) ####################################### # Drag and Drop ####################################### def on_playlist_drag(self, widget, context, data=None): """When the user changes the order of the playlist.""" list = [] i = 0 cur_song_num = None for song in self.playlist_list_store: # iterate the rows song_id = song[2] list.append(song_id) if song[0] is self.images_pixbuf_playing: # this row has a now playing icon cur_song_num = i i += 1 self.audio_engine.set_playlist(list) if cur_song_num != None: # song is playing self.audio_engine.set_current_song(cur_song_num) ####################################### # Misc Selection Methods ####################################### def on_time_elapsed_slider_change(self, slider): """When the user moves the seek bar.""" seek_time_secs = slider.get_value() human_readable = helperfunctions.convert_seconds_to_human_readable(seek_time_secs) gobject.idle_add(self.time_seek_label.set_text, human_readable) if self.audio_engine.seek(seek_time_secs): print "Seek to %s successful" % human_readable else: print "Seek to %s failed!" % human_readable return True def on_time_elapsed_slider_change_value(self, slider, data1=None, data2=None): """When the user drags the slide bar but doesn't commit yet""" seek_time_secs = slider.get_value() gobject.idle_add(self.time_seek_label.set_text, helperfunctions.convert_seconds_to_human_readable(seek_time_secs)) def on_volume_slider_change(self, range, scroll, value): """Change the volume.""" self.audio_engine.set_volume(value) ####################################### # Button Clicked Methods ####################################### def button_save_preferences_clicked(self, widget, data=None): """When the save button is pressed in the preferences""" window = data url = self.ampache_text_entry.get_text() username = self.username_text_entry.get_text() password = self.password_text_entry.get_text() # check to see if any of the fields have been edited, if so, reauth with new credentials try: if url == self.ampache_conn.url and username == self.ampache_conn.username and password == self.ampache_conn.password: window.destroy() return True # haven't changed except: pass # if the code makes it this far, the credentials have been changed self.stop_all_threads() self.audio_engine.clear_playlist() self.clear_album_art() dbfunctions.clear_cached_catalog(self.db_session) self.ampache_conn.set_credentials(username, password, url) # credentials saved if self.ampache_conn.has_credentials: # credentials are NOT blank self.db_session.variable_set('credentials_username', username) self.db_session.variable_set('credentials_password', password) self.db_session.variable_set('credentials_url', url) self.update_statusbar(_("Saved Credentials")) print _("Credentials Saved") self.destroy_settings(window) self.login_and_get_artists("changed") else: self.update_statusbar(_("Couldn't save credentials!")) print _("[Error] Couldn't save credentials!") return False return True def button_cancel_preferences_clicked(self, widget, data=None): """Destroy the preferences window.""" window = data self.destroy_settings(window) def button_open_downloads_file_chooser_clicked(self, widget, data=None): """Open file chooser for the downloads directory.""" dialog = gtk.FileChooserDialog(_("Choose Folder..."), None, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) dialog.set_current_folder(os.path.expanduser("~")) dialog.set_default_response(gtk.RESPONSE_OK) response = dialog.run() if response == gtk.RESPONSE_OK: self.downloads_directory = dialog.get_current_folder() self.downloads_text_entry.set_text(self.downloads_directory) self.db_session.variable_set('downloads_directory', self.downloads_directory) dialog.destroy() def button_reauthenticate_clicked(self, widget=None, data=None): """Reauthenticate button clicked.""" self.login_and_get_artists(True) def button_play_pause_clicked(self, widget=None, data=None): """The play/pause has been clicked.""" state = self.audio_engine.get_state() if state == "stopped" or state == None: return False elif state == "playing": self.audio_engine.pause() self.play_pause_image.set_from_pixbuf(self.images_pixbuf_play) return "paused" #self.set_tray_icon(None) else: if self.audio_engine.play(): self.play_pause_image.set_from_pixbuf(self.images_pixbuf_pause) #self.set_tray_icon(self.album_art_image.get_pixbuf()) return "playing" def button_prev_clicked(self, widget=None, data=None): """Previous Track.""" time_nanoseconds = self.audio_engine.query_position() if time_nanoseconds != -1: time_seconds = time_nanoseconds / 1000 / 1000 / 1000 if time_seconds <= 5: # go back if time is less than 5 seconds return self.audio_engine.prev_track() else: # restart the song return self.audio_engine.restart() # failsafe return self.audio_engine.prev_track() def button_next_clicked(self, widget=None, data=None): """Next Track.""" return self.audio_engine.next_track() ############# # Playlists ############# def button_save_playlist_clicked(self, widget, data=None): """The save playlist button was clicked.""" if not self.audio_engine.get_playlist(): self.create_dialog_alert("error", _("Cannot save empty playlist."), True) print _("Cannot save empty playlist") return False self.show_playlist_select('Save') return False def button_load_playlist_clicked(self, widget, data=None): """The load playlist button was clicked.""" self.show_playlist_select('Load') return False def button_export_playlist_clicked(self, widget, data=None): self.show_playlist_select('Export') return False def button_load_or_save_playlist_clicked(self, widget, selection, text, type): """When the user wants to load or save a playlist from Ampache.""" playlist_list_store, iter = selection.get_selected() text = text.get_text() if iter == None: playlist_id = -1 playlist_name = '' else: playlist_id = playlist_list_store[iter][4] playlist_name = playlist_list_store[iter][0] list = [] if type == 'Load': # Load playlist if iter == None: # nothing selected return True if playlist_id == -1: # text place holders return True if playlist_id == -2: # Local Playlists list = dbfunctions.get_playlist(self.db_session, playlist_name) else: playlist = self.ampache_conn.get_playlist_songs(playlist_id) if not playlist: print "Error with playlist %d" % playlist_id self.create_dialog_alert("error", _("Problem loading playlist. Playlist ID = %d" % playlist_id), True) return True for song in playlist: list.append(song['song_id']) self.load_playlist(list) self.destroy_playlist() elif type == 'Save': # Save playlist if not self.audio_engine.get_playlist(): # playlist is empty self.create_dialog_alert("error", _("Cannot save empty playlist."), True) print _("Cannot save empty playlist.") return False if text == '': # no name for list was specified self.create_dialog_alert("error", _("Invalid Name."), True) return False if dbfunctions.get_playlist(self.db_session, text): # playlist exists answer = self.create_dialog_ok_or_close(_("Overwrite Playlist?"), _("A playlist by the name '%s' already exists, overwrite?" % text)) if answer != "ok": return False dbfunctions.set_playlist(self.db_session, text, self.audio_engine.get_playlist()) self.destroy_playlist() elif type == 'Export': # Export Playlist if iter == None: # nothing selected return True if playlist_id == -1: # placeholders return True if playlist_id != -2: # not local playlists self.create_dialog_alert("error", _("Only exporting of local playlists is supported."), True) return False else: # ready to export list = dbfunctions.get_playlist(self.db_session, playlist_name) if not list: self.create_dialog_alert("error", _("Cannot export empty playlist."), True) return False chooser = gtk.FileChooserDialog(title="Save as...",action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK)) chooser.set_current_folder(os.path.expanduser("~")) response = chooser.run() if response == gtk.RESPONSE_OK: filename = chooser.get_filename() if os.path.isfile(filename): self.create_dialog_alert("error", _("File already exists."), True) chooser.destroy() return False f = open(filename, 'w') f.write("Playlist '%s' export from Viridian - %s\n\n" % (playlist_name, time.strftime("%A, %B %d, %Y at %I:%M%p"))) for song_id in list: f.write(helperfunctions.convert_html_to_string(self.ampache_conn.get_song_url(song_id).rpartition('.')[0])) f.write("\n") f.close() print "Exported playlist to %s" % (filename) self.create_dialog_alert("info", _("Playlist %s written to %s." % (playlist_name, filename)), True) chooser.destroy() self.destroy_playlist() def button_delete_playlist_clicked(self, widget, selection, type): playlist_list_store, iter = selection.get_selected() if iter == None: return False playlist_id = playlist_list_store[iter][4] playlist_name = playlist_list_store[iter][0] if playlist_id == -2: # Local Playlists answer = self.create_dialog_ok_or_close(_("Delete Playlist?"), _("Are you sure you want to delete the playlist '%s'?" % playlist_name)) if answer != "ok": return False dbfunctions.remove_playlist(self.db_session, playlist_name) self.update_playlist_select(type, playlist_list_store) elif playlist_id != -1: # Ampache playlists self.create_dialog_alert('error', _("Cannot delete playlists that are on the Ampache server from Viridian.")) ############# # Cache ############# def button_clear_cached_artist_info_clicked(self, widget=None, data=None): """Clear local cache.""" try: # check to see if this function is running if self.button_clear_cache_locked == True: print "Already Running" return False except: pass self.button_clear_cache_locked = True print "Clearing cached catalog -- will reauthenticate and pull artists" self.stop_all_threads() dbfunctions.clear_cached_catalog(self.db_session) #self.audio_engine.stop() self.db_session.variable_set('current_playlist', self.audio_engine.get_playlist()) self.login_and_get_artists() self.button_clear_cache_locked = False def button_clear_album_art_clicked(self, widget=None, data=None): """Clear local album art.""" self.clear_album_art() self.update_statusbar(_("Album Art Cleared")) def button_reset_everything_clicked(self, widget=None, data=None): """Reset everything.""" answer = self.create_dialog_ok_or_close(_("Reset Viridian"), _("Are you sure you want to delete all personal information stored with Viridian?")) if answer == "ok": self.reset_everything() gtk.main_quit() def button_pre_cache_info_clicked(self, widget=None, data=None): """Pre-cache all album and song info.""" if self.ampache_conn.is_authenticated() == False: self.create_dialog_alert("warn", _("Not Authenticated"), True) return False try: # check to see if this function is running if self.button_pre_cache_locked == True: print "Already Running" self.create_dialog_alert("info", _("Pre-Cache already in progress.")) return False except: pass answer = self.create_dialog_ok_or_close(_("Pre-Cache"), _("This will cache all of the artist, album, and song information (not the songs themselves) locally to make Viridian respond faster.\n\nThis process can take a long time depending on the size of your catalog. Proceed?")) if answer != "ok": return False self.button_pre_cache_locked = True gobject.idle_add(self.__button_pre_cache_info_clicked) #thread.start_new_thread(self.__button_pre_cache_info_clicked, (None,)) def __button_pre_cache_info_clicked(self, widget=None, data=None): self.pre_cache_continue = True # this will be set to false if this function should stop try: start_time = int(time.time()) artists = dbfunctions.get_artist_ids(self.db_session) i = 0 num_artists = len(artists) for artist_id in artists: i += 1 if self.pre_cache_continue == False: self.button_pre_cache_locked = False return False self.check_and_populate_albums(artist_id) self.update_statusbar(_("Pulling all albums from artists: %d/%d" % (i, num_artists) )) #gobject.idle_add(self.update_statusbar, 1, "Pulling all albums from artists: %d/%d" % (i, num_artists) ) self.update_statusbar(_("Finished pulling albums")) albums = dbfunctions.get_album_ids(self.db_session) i = 0 num_albums = len(albums) for album_id in albums: i += 1 if self.pre_cache_continue == False: self.button_pre_cache_locked = False return False self.check_and_populate_songs(album_id) self.update_statusbar(_("Pulling all songs from albums: %d/%d" % (i, num_albums) ) ) end_time = int(time.time()) time_taken = end_time - start_time time_taken = helperfunctions.convert_seconds_to_human_readable(time_taken) self.update_statusbar(_("Finished Pre Cache -- Time Taken: " + str(time_taken))) print "Finished Pre Cache -- Time Taken: " + str(time_taken) except Exception, detail: print "Error with pre-cache!", detail self.update_statusbar(_("Error with pre-cache!")) self.button_pre_cache_locked = False self.create_dialog_alert("error", _("Error with pre-cache!\n\n"+str(detail) ) ) return False self.button_pre_cache_locked = False return False def button_album_art_clicked(self, widget, event=None, data=None): """Handle event box events for the album art.""" if event.button == 3: self.__button_album_art_right_clicked(widget, event, data) # right click else: self.__button_album_art_left_clicked(widget, event, data) # left click def __button_album_art_left_clicked(self, widget, event, data): """Left click on album art.""" self.__re_fetch_album_art() def __button_album_art_right_clicked(self, widget, event, data): """Right click on album art.""" # create popup m = gtk.Menu() i = gtk.MenuItem(_("Open Image")) i.connect('activate', lambda x: self.gnome_open(self.current_album_art_file)) m.append(i) i = gtk.MenuItem(_("Refresh Album Art")) i.connect('activate', self.__re_fetch_album_art) m.append(i) m.show_all() m.popup(None, None, None, event.button, event.time, None) return False ################## # XML Server Buttons ################## def button_xml_server_clicked(self, widget, action, label, image, port): """Start, stop or restart the xml server.""" if self.xml_server.is_running: # xml server is running if action == "start": return False else: if action == "stop": return False if port.get_text().isdigit(): self.db_session.variable_set('xmlrpc_port', int(port.get_text())) if action == "start": self.start_xml_server() elif action == "stop": self.stop_xml_server() elif action == "restart": self.restart_xml_server() # update the gui if self.xml_server.is_running: image.set_from_stock(gtk.STOCK_YES,gtk.ICON_SIZE_SMALL_TOOLBAR) label.set_text(_("Running. (port %d)" % self.xml_server.port)) else: image.set_from_stock(gtk.STOCK_NO,gtk.ICON_SIZE_SMALL_TOOLBAR) label.set_text(_("Not Running.")) ####################################### # Dialogs ####################################### def create_dialog_alert(self, dialog_type, message, ok=False): """Creates a generic dialog of the type specified with close.""" if dialog_type == "warn": dialog_type = gtk.MESSAGE_WARNING elif dialog_type == "error": dialog_type = gtk.MESSAGE_ERROR elif dialog_type == "info": dialog_type = gtk.MESSAGE_INFO elif dialog_type == "question": dialog_type = gtk.MESSAGE_QUESTION else: return False if ok == True: # display OK button md = gtk.MessageDialog(self.window, gtk.DIALOG_DESTROY_WITH_PARENT, dialog_type, gtk.BUTTONS_OK, message) else: # display Close button md = gtk.MessageDialog(self.window, gtk.DIALOG_DESTROY_WITH_PARENT, dialog_type, gtk.BUTTONS_CLOSE, message) md.set_title('Viridian') md.set_icon(self.images_pixbuf_viridian_simple) md.run() md.destroy() def create_dialog_ok_or_close(self, title, message): """Creates a generic dialog of the type specified with ok and cancel.""" md = gtk.Dialog(str(title), self.window, gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK)) label = gtk.Label(message) label.set_line_wrap(True) md.get_child().pack_start(label) md.get_child().set_border_width(10) md.set_border_width(3) md.set_resizable(False) md.set_icon(self.images_pixbuf_viridian_simple) md.show_all() #md.set_title('Viridian') resp = md.run() md.destroy() if resp == gtk.RESPONSE_OK: return "ok" else: return "cancel" def create_about_dialog(self, widget, data=None): """About this application.""" about = gtk.AboutDialog() about.set_name("Viridian") about.set_icon(self.images_pixbuf_viridian_simple) about.set_version(VERSION_NUMBER) about.set_copyright("(c) Dave Eddy ") about.set_comments(_("Viridian is a front-end for an Ampache Server (see http://ampache.org)")) about.set_website("http://viridian.daveeddy.com") about.set_authors(["Author:", "Dave Eddy ", "http://www.daveeddy.com", "", "AudioEngine by:", "Michael Zeller ", "http://conquerthesound.com"]) about.set_artists(["Skye Sawyer ", "http://www.skyeillustration.com", "", "Media Icons by:", "http://mysitemyway.com", "http://ampache.org"]) try: # try to set the logo about.set_logo(self.images_pixbuf_viridian_app) except: pass gpl = "" try: # try to read the GPL, if not, just paste the link h = open(os.path.join(SCRIPT_PATH, 'doc' + os.sep + 'gpl.txt')) s = h.readlines() for line in s: gpl += line except: gpl = "GPL v3 " about.set_license(gpl) about.run() about.destroy() def create_catalog_updated_dialog(self): """Create a dialog to tell the user the cache has been updated.""" answer = self.create_dialog_ok_or_close(_("Ampache Catalog Updated"), _("The Ampache catalog on the server is newer than the locally cached catalog on this computer.\nWould you like to update the local catalog by clearing the local cache?\n\n(You can also do this at anytime by going to File -> Clear Local Cache).")) if answer == "ok": return True return False ####################################### # Audio Engine Callback ####################################### def audioengine_song_changed(self, song_id): """The function that gets called when the AudioEngine changes songs.""" if song_id != None: if dbfunctions.song_has_info(self.db_session, song_id): self.current_song_info = dbfunctions.get_single_song_dict(self.db_session, song_id) else: self.current_song_info = self.ampache_conn.get_song_info(song_id) gobject.idle_add(self.__audioengine_song_changed, song_id) def __audioengine_song_changed(self, song_id): """The function that gets called when the AudioEngine changes songs.""" self.update_playlist_window() self.refresh_gui() if song_id == None: # nothing playing self.current_song_info = None self.play_pause_image.set_from_pixbuf(self.images_pixbuf_play) self.set_tray_tooltip('Viridian') self.window.set_title("Viridian") self.set_tray_icon(None) #self.update_playlist_window() return False self.play_pause_image.set_from_pixbuf(self.images_pixbuf_pause) print self.current_song_info # DEBUG song_time = self.current_song_info['song_time'] self.time_elapsed_slider.set_range(0, song_time) self.time_total_label.set_text(helperfunctions.convert_seconds_to_human_readable(song_time)) song_title = self.current_song_info['song_title'] artist_name = self.current_song_info['artist_name'] album_name = self.current_song_info['album_name'] song_title_html = helperfunctions.convert_string_to_html(song_title) artist_name_html = helperfunctions.convert_string_to_html(artist_name) album_name_html = helperfunctions.convert_string_to_html(album_name) ### Update EVERYTHING to say the current artist, album, and song # make font size smaller if the title is long length = len(song_title_html) if length > 40: self.current_song_label.set_markup(''+song_title_html+'') elif length > 20: self.current_song_label.set_markup(''+song_title_html+'') else: self.current_song_label.set_markup(''+song_title_html+'') self.current_artist_label.set_markup( ''+artist_name_html+'' ) self.current_album_label.set_markup( ''+album_name_html+'' ) ### Update the statusbar and tray icon ### self.set_tray_tooltip("Viridian :: " + song_title + ' - ' + artist_name + ' - ' + album_name) self.update_statusbar(song_title + ' - ' + artist_name + ' - ' + album_name) self.window.set_title("Viridian :: " + song_title + ' - ' + artist_name + ' - ' + album_name) ### Get the album Art ### album_id = self.current_song_info['album_id'] if not os.path.exists(ALBUM_ART_DIR): os.mkdir(ALBUM_ART_DIR) self.current_album_art_file = os.path.join(ALBUM_ART_DIR, str(album_id)) if os.path.isfile(self.current_album_art_file): print "Album art exists locally" else: print "Fetching album art... ", album_art = self.ampache_conn.get_album_art(album_id) response = urllib2.urlopen(album_art) f = open(self.current_album_art_file, 'w') f.write(response.read()) f.close() print "Done!" # now create a pixel buffer for the image and set it in the GUI image_pixbuf = guifunctions.create_image_pixbuf(self.current_album_art_file, ALBUM_ART_SIZE) self.album_art_image.set_from_pixbuf(image_pixbuf) self.set_tray_icon(image_pixbuf) self.refresh_gui() ### Send notifications OSD ### self.notification(_("Now Playing"), song_title + ' - ' + artist_name + ' - ' + album_name, self.current_album_art_file) # rating stars stars = self.current_song_info['precise_rating'] i = 0 while i < 5: if stars > i: self.rating_stars_list[i].set_from_pixbuf(self.images_pixbuf_gold_star) else: self.rating_stars_list[i].set_from_pixbuf(self.images_pixbuf_gray_star) i += 1 ### Alert the plugins! ### thread.start_new_thread(self._alert_plugins_of_song_change, (None,)) def _alert_plugins_of_song_change(self, *args): """Fire off enabled plugins because the song has changed""" for name, plugin in self.plugins.iteritems(): if name in self.enabled_plugins: # only fire off enabled plugins try: print "Alerting plugin '%s' of song change" % (name) plugin.on_song_change(self.current_song_info) print "Plugin exited successfully" except: print "Error with plugin '%s':\n+++++++ BEGIN STACK TRACE ++++++++" % (name) traceback.print_exc() # DEBUG print "++++++++ END STACK TRACE +++++++++" pass def audioengine_error_callback(self, error_message): """Display the gstreamer error in the notification label.""" self.update_statusbar(_("An error has occured.")) self.create_dialog_alert('warn', _("""GStreamer has encountered an error, this is most likely caused by: - gstreamer-plugins not being installed. - Ampache not transcoding the file correctly. - A lost or dropped connection to the server. Message from GStreamer: %s""" % error_message)) ####################################### # Convenience Functions ####################################### ############ # check database for info ############ def check_and_populate_artists(self): """Returns an artist list by either grabbing from the DB or from Ampache.""" if self.db_session.table_is_empty('artists'): artists = self.ampache_conn.get_artists() if artists == None: return False list = [] for artist in artists: custom_artist_name = re.sub('^the |^a ', '', artist['artist_name'].lower()) list.append([artist['artist_id'], artist['artist_name'], custom_artist_name]) dbfunctions.populate_artists_table(self.db_session, list) def check_and_populate_albums(self, artist_id=-1): if artist_id == -1: if self.db_session.table_is_empty('albums'): albums = self.ampache_conn.get_albums() if albums == None: return False list = [] for album in albums: list.append([album['artist_id'], album['album_id'], album['album_name'], album['album_year'], album['precise_rating']]) dbfunctions.populate_full_albums_table(self.db_session, list) else: if dbfunctions.table_is_empty(self.db_session, 'albums', artist_id): albums = self.ampache_conn.get_albums_by_artist(artist_id) if albums == None: return False list = [] for album in albums: list.append([artist_id, album['album_id'], album['album_name'], album['album_year'] , album['precise_rating']]) dbfunctions.populate_albums_table(self.db_session, artist_id, list) def check_and_populate_songs(self, album_id): if dbfunctions.table_is_empty(self.db_session, 'songs', album_id): songs = self.ampache_conn.get_songs_by_album(album_id) if songs == None: return False list = [] for song in songs: list.append([album_id, song['song_id'], song['song_title'], song['song_track'], song['song_time'], song['song_size'], song['artist_name'], song['album_name']]) dbfunctions.populate_songs_table(self.db_session, album_id, list) ################ # Random Helpers ################ def gnome_open(self, uri): """Open with gnome-open.""" if uri == None or uri == "": self.create_dialog_alert("warn", _("The file/URL specified is invalid."), True) else: os.popen("gnome-open '%s' &" % (uri)) def update_statusbar(self, text): """Update the status bar and run pending main_iteration() events.""" try: # try to pop off any text already on the bar self.statusbar.pop(0) except: pass self.statusbar.push(0, text) self.refresh_gui() def notification(self, title, message=None, image=None): """Display OSD notifications if the user wants them and it's installed.""" if PYNOTIFY_INSTALLED and self.display_notifications: if message == None: message = title title = 'Viridian' if image == None: image = IMAGES_DIR + 'ViridianApp.png' pynotify_object.update(title, message, image) pynotify_object.show() def set_tray_tooltip(self, message): """Set the tooltip of the tray icon if it is set.""" if hasattr(self, 'tray_icon'): self.tray_icon.set_tooltip(message) return True return False def set_tray_icon(self, pixbuf): """Set the tray icon to a pixbuf.""" if hasattr(self, 'tray_icon'): if pixbuf == None: self.tray_icon.set_from_pixbuf(self.images_pixbuf_viridian_app) else: self.tray_icon.set_from_pixbuf(pixbuf) return True return False def stop_all_threads(self): """Stops all running threads.""" self.pre_cache_continue = False self.continue_load_songs = False def refresh_gui(self): """Refresh the GUI by calling gtk.main_iteration(). """ while gtk.events_pending(): gtk.main_iteration() def copy_text_to_clipboard(self, text): """Copies the content of 'text' to the clipboard""" c = gtk.Clipboard() c.set_text(text) def copy_song_url_to_clipboard(self, song_id): """Copies the song_id's URL to the clipboard""" self.copy_text_to_clipboard(self.ampache_conn.get_song_url(song_id)) #################### # Playlist Functions #################### def update_playlist_window(self): """Updates the playlist window with the current playing songs.""" gobject.idle_add(self.__update_playlist_window) def __update_playlist_window(self): cur_playlist = self.audio_engine.get_playlist() cur_song_num = self.audio_engine.get_current_song() list = [] i = 0 for song_id in cur_playlist: cur_song = {} if dbfunctions.song_has_info(self.db_session, song_id): cur_song = dbfunctions.get_playlist_song_dict(self.db_session, song_id) else: cur_song = self.ampache_conn.get_song_info(song_id) cur_string = cur_song['song_title'] + ' - ' + cur_song['artist_name'] + ' - ' + cur_song['album_name'] cur_string = helperfunctions.convert_string_to_html(cur_string) now_playing = self.images_pixbuf_empty if i == cur_song_num: now_playing = self.images_pixbuf_playing cur_string = '' + cur_string + '' list.append([now_playing, cur_string, song_id]) i += 1 self.refresh_gui() self.playlist_list_store.clear() for string in list: self.playlist_list_store.append(string) return False def load_playlist(self, list): """Takes a list of song_ids and loads it into the audio engine.""" self.audio_engine.clear_playlist() self.audio_engine.set_playlist(list) self.update_statusbar(_('Loading Playlist...')) i = 1 print list for song in list: self.update_statusbar(_('Querying for song %d/%d in playlist') % (i, len(list))) if not dbfunctions.song_has_info(self.db_session, song): song = self.ampache_conn.get_song_info(song) self.check_and_populate_albums(song['artist_id']) self.check_and_populate_songs( song['album_id']) i += 1 self.update_playlist_window() self.update_statusbar(_('Playlist loaded')) def add_album_to_playlist(self, widget): """Adds every song in the visible list store and adds it to the playlist.""" for song in self.song_list_store: self.audio_engine.insert_into_playlist(song[6]) self.update_playlist_window() return True def add_songs_to_playlist(self, widget, list): for song_id in list: self.add_song_to_playlist(widget, song_id) def add_song_to_playlist(self, widget, song_id): """Takes a song_id and adds it to the playlist.""" self.audio_engine.insert_into_playlist(song_id) self.update_playlist_window() return True def remove_from_playlist(self, widget, song_id, treeview, list=None): """Remove a song from the current playlist.""" if list != None: for song_id in list: self.remove_from_playlist(widget, song_id, treeview, None) #print self.audio_engine.get_current_song() return True else: if self.audio_engine.remove_from_playlist(song_id): self.update_playlist_window() return True return False ################## # XML Server ################## def start_xml_server(self): """Start the XML Server.""" self.restart_xml_server() def restart_xml_server(self): """Restart the XML server.""" self.stop_xml_server() time.sleep(1) xmlrpc_port = self.db_session.variable_get('xmlrpc_port', XML_RPC_PORT) self.current_xmlrpc_port = xmlrpc_port self.xml_server = XMLServer('', xmlrpc_port) ### Load the functions ### self.xml_server.register_function(self.button_next_clicked, 'next') self.xml_server.register_function(self.button_prev_clicked, 'prev') self.xml_server.register_function(self.button_play_pause_clicked, 'play_pause') self.xml_server.register_function(self.increment_volume, 'volume_up') self.xml_server.register_function(self.decrement_volume, 'volume_down') self.xml_server.register_function(self.set_volume, 'set_volume') self.xml_server.register_function(self.get_current_song, 'get_current_song') self.xml_server.register_function(self.audio_engine.get_state, 'get_state') self.xml_server.register_function(self.audio_engine.get_volume, 'get_volume') #self.xml_server.register_function(lambda : self.ampache_conn.get_album_art(self.current_song_info['album_id']), 'get_album_art') self.xml_server.serve_forever() def stop_xml_server(self): """Stop the XML Server.""" try: self.xml_server.shutdown() except: pass ################### # Func for XML ################### def get_current_song(self, *args): """Get the current song info.""" return self.current_song_info ################### # Volume modifiers ################### def increment_volume(self, *args): """Increment the volume by 5%.""" cur_vol = self.audio_engine.get_volume() vol = cur_vol + 5 return self.set_volume(vol) def decrement_volume(self, *args): """Decrement the volume by 5%.""" cur_vol = self.audio_engine.get_volume() vol = cur_vol - 5 return self.set_volume(vol) def set_volume(self, vol, *args): """Set the volume, assumes the value is 0= 100: vol = 100 elif vol <= 0: vol = 0 self.volume_slider.set_value(vol) self.audio_engine.set_volume(vol) return self.audio_engine.get_volume() ####################################### # Download Songs / Albums ####################################### def download_songs_clicked(self, widget, list): """The user is downloading multiple songs from the playlist.""" if not os.path.exists(self.downloads_directory): self.create_dialog_alert("warn", _("The folder %s does not exist. You can change the folder in Preferences.") % (self.downloads_directory), True) return False if self.show_downloads_checkbox.active == False: self.side_panel.show() self.downloads_window.show() self.show_downloads_checkbox.set_active(True) for song_id in list: self.download_song_clicked(widget, song_id, False) def download_album_clicked(self, widget): """The user cliked download album.""" # check to see if the downloads directory exists if not os.path.exists(self.downloads_directory): self.create_dialog_alert("warn", _("The folder %s does not exist. You can change the folder in Preferences.") % (self.downloads_directory), True) return False if self.show_downloads_checkbox.active == False: self.side_panel.show() self.downloads_window.show() self.show_downloads_checkbox.set_active(True) for song in self.song_list_store: self.download_song_clicked(widget, song[6], False) def download_song_clicked(self, widget, song_id, show_panel=True): """The user clicked download song.""" # check to see if the downloads directory exists if not os.path.exists(self.downloads_directory): self.create_dialog_alert("warn", _("The folder %s does not exist. You can change the folder in Preferences.") % (self.downloads_directory), True) return False if show_panel and self.show_downloads_checkbox.active == False: self.side_panel.show() self.downloads_window.show() self.show_downloads_checkbox.set_active(True) song_url = self.ampache_conn.get_song_url(song_id) m = re.search('name=.*\.[a-zA-Z0-9]+', song_url) song_string = helperfunctions.convert_html_to_string(m.group(0).replace('name=/','')) full_file = os.path.join(self.downloads_directory, song_string) self.downloads_list_store.append([song_string, 0, full_file]) iter1 = self.downloads_list_store.get_iter(len(self.downloads_list_store) - 1) thread.start_new_thread(self.download_song, (song_url, full_file, iter1)) def download_song(self, url, dst, iter1): THREAD_LOCK.acquire() print "get url '%s' to '%s'" % (url, dst) urllib.urlretrieve(url, dst, lambda nb, bs, fs, url=url: self._reporthook(nb,bs,fs,url,iter1)) self.notification(_("Download Complete"), os.path.basename(url).replace('%20',' ').replace('%27', "'")) THREAD_LOCK.release() #urllib.urlretrieve(url, dst, self._reporthook, iter1) def _reporthook(self, numblocks, blocksize, filesize, url, iter1): #print "reporthook(%s, %s, %s)" % (numblocks, blocksize, filesize) base = os.path.basename(url).replace('%20',' ').replace('%27', "'") #XXX Should handle possible filesize=-1. try: percent = min((numblocks*blocksize*100)/filesize, 100) except: percent = 100 if numblocks != 0: #self.update_statusbar("Downloading " + base + ": " + str(percent) + "%") gobject.idle_add(lambda : self.downloads_list_store.set(iter1, 1, percent)) ####################################### # Resets ####################################### def clear_album_art(self): """Clear local album art.""" if os.path.exists(ALBUM_ART_DIR): print "+++ Checking for album art +++" for root, dirs, files in os.walk(ALBUM_ART_DIR): for name in files: print "Deleting ", os.path.join(root, name) os.remove(os.path.join(root, name)) def reset_everything(self): """Delete all private/personal data from the users system.""" self.stop_all_threads() try: shutil.rmtree(VIRIDIAN_DIR) os.rmdir(VIRIDIAN_DIR) except: pass ####################################### # Threads ####################################### def query_position(self, data=None): """Thread that updates the label and the seek/slider.""" self.time_seek_label.set_text(" ") new_time_nanoseconds = self.audio_engine.query_position() if new_time_nanoseconds != -1: new_time_seconds = new_time_nanoseconds / 1000 / 1000 / 1000 new_time_human_readable = helperfunctions.convert_seconds_to_human_readable(new_time_seconds) for signal in self.time_elapsed_signals: self.time_elapsed_slider.handler_block(signal) self.time_elapsed_slider.set_value(new_time_seconds) for signal in self.time_elapsed_signals: self.time_elapsed_slider.handler_unblock(signal) self.time_elapsed_label.set_text(new_time_human_readable) return True def keep_session_active(self, data=None): """Thread to keep the session active when a song is paused (DOESN'T WORK YET).""" if self.audio_engine.get_state() == 'paused': self.ampache_conn.ping() return True ####################################### # Private Methods ####################################### ################# # Internal Helper Functions ################# def __authenticate(self, data=None): self.__successfully_authed = self.ampache_conn.authenticate() return True def __clear_all_list_stores(self): """Clears all list stores in the GUI.""" self.song_list_store.clear() self.album_list_store.clear() self.artist_list_store.clear() def __re_fetch_album_art(self, data=None): try: # check to see if this function is running if self.button_album_art_locked == True: print "Already Running" return False except: pass self.button_album_art_locked = True print "Re-Fetching album art... ", self.update_statusbar(_("Re-Fetching album art...")) if not os.path.exists(ALBUM_ART_DIR): os.mkdir(ALBUM_ART_DIR) try: # attempt to get the current playing songs album art album_id = self.current_song_info['album_id'] art_file = os.path.join(ALBUM_ART_DIR, str(album_id)) album_art = self.ampache_conn.get_album_art(album_id) response = urllib2.urlopen(album_art) f = open(art_file, 'w') f.write(response.read()) f.close() except: # cache was cleared or something and it fails... self.update_statusbar(_("Re-Fetching album art... Failed!")) print "Failed!" self.button_album_art_locked = False return False image_pixbuf = guifunctions.create_image_pixbuf(art_file, ALBUM_ART_SIZE) self.album_art_image.set_from_pixbuf(image_pixbuf) self.set_tray_icon(image_pixbuf) print "Done!" self.update_statusbar(_("Re-Fetching album art... Success!")) self.button_album_art_locked = False return True def __add_songs_to_list_store(self, album_id): """Takes an album_id, and adds all of that albums songs to the GUI.""" self.check_and_populate_songs(album_id) songs = dbfunctions.get_song_dict(self.db_session, album_id) model = self.song_list_store if not songs: print "Error pulling ", album_id self.update_statusbar(_("Error with album -- Check Ampache -- Album ID = %d" % album_id)) return False for song in songs: song_track = songs[song]['track'] song_title = songs[song]['title'] song_time = songs[song]['time'] song_size = songs[song]['size'] song_id = song artist_name = songs[song]['artist_name'] album_name = songs[song]['album_name'] #album_year = self.ampache_conn.get_album_year(album_id)S # convert time in seconds to HH:MM:SS THIS WILL FAIL IF LENGTH > 24 HOURS song_time = time.strftime('%H:%M:%S', time.gmtime(song_time)) if song_time[:2] == "00": # strip out hours if below 60 minutes song_time = song_time[3:] # convert size to humand_readable song_size = helperfunctions.convert_filesize_to_human_readable(float(song_size)) model.append([song_track, song_title, artist_name, album_name, song_time, song_size, song_id]) return True def __find_plugins(self, plugin_dir): """ Taken From http://www.luckydonkey.com/2008/01/02/python-style-plugins-made-easy/ """ plugins_list = [] for root, dirs, files in os.walk(plugin_dir): for name in files: if name.endswith(".py") and not name.startswith("__"): plugins_list.append(name.rsplit('.', 1)[0]) return plugins_list def __import_plugin(self, plugin_name): """Import the given plugin""" sys.path.append(PLUGINS_DIR) try: module = __import__(plugin_name) plugin = module.__init__() except: plugin = None sys.path.remove(PLUGINS_DIR) return plugin viridian-1.2/AmpacheTools/helperfunctions.py0000755000175000017500000001056311511672746021144 0ustar charliecharlie#!/usr/bin/env python # -*- coding: utf-8 -*- ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE import time import gtk import urllib """ Misc. functions for AmpacheGUI """ ################# # Formatters ################# def convert_filesize_to_human_readable(bytes): """Converts bytes to humand_readable form.""" if bytes >= 1073741824: return str(round(bytes / 1024 / 1024 / 1024, 1)) + ' GB' elif bytes >= 1048576: return str(round(bytes / 1024 / 1024, 1)) + ' MB' elif bytes >= 1024: return str(round(bytes / 1024, 1)) + ' KB' elif bytes < 1024: return str(bytes) + ' bytes' return str(bytes) def convert_seconds_to_human_readable(seconds): """Converts seconds to a human readable string.""" if seconds == 0: return "0:00" # convert time in seconds to HH:MM:SS THIS WILL FAIL IF LENGTH > 24 HOURS new_time = time.strftime('%H:%M:%S', time.gmtime(seconds)) if new_time[:3] == "00:": # strip out hours if below 60 minutes new_time = new_time[3:] if new_time[:3] == "00:": # convert 00:xx to 0:x new_time = new_time[1:] return new_time def convert_string_to_html(string): """Change characters to HTML friendly versions.""" return string.replace('&', '&') def convert_html_to_string(html): """Replace HTML characters to their normal character counterparts.""" return urllib.url2pathname(html.replace('&', '&')) ################# # Sort Functions ################# def sort_artists_by_custom_name(model, iter1, iter2, column): """Custom Function to sort artists by extracting words like "the" and "a".""" id1 = model[iter1][1] id2 = model[iter2][1] band1 = model[iter1][2] band2 = model[iter2][2] order = column.get_sort_order() # First check for -1 artist (always top row) if id1 == -1: if order == gtk.SORT_DESCENDING: return 1 else: return -1 elif id2 == -1: if order == gtk.SORT_DESCENDING: return -1 else: return 1 # sort alphabetically if band1 < band2: return -1 elif band1 > band2: return 1 return 0 def sort_albums_by_year(model, iter1, iter2, column): """Custom function to sort albums by year.""" year1 = model[iter1][2] year2 = model[iter2][2] order = column.get_sort_order() # First check for -1 album (always top row) if year1 == -1: if order == gtk.SORT_DESCENDING: return 1 else: return -1 elif year2 == -1: if order == gtk.SORT_DESCENDING: return -1 else: return 1 # otherwise organize them by their years if year1 < year2: return -1 elif year1 > year2: return 1 else: name1 = model[iter1][0] name2 = model[iter2][0] if name1 < name2: return -1 elif name1 > name2: return 1 return 0 def sort_songs_by_title(model, iter1, iter2, data=None): """Custom function to sort titles alphabetically.""" title1 = model[iter1][1] title2 = model[iter2][1] if title1 < title2: return -1 elif title2 < title1: return 1 return 0 def sort_songs_by_track(model, iter1, iter2, data=None): """Custom function to sort songs by track.""" track1 = model[iter1][0] track2 = model[iter2][0] if track1 < track2: return -1 elif track1 > track2: return 1 return sort_songs_by_title(model, iter1, iter2, data) def sort_songs_by_album(model, iter1, iter2, data=None): """Custom function to sort songs by album, if the albums are the same it will sort by tracks.""" album1 = model[iter1][3] album2 = model[iter2][3] if album1 < album2: return -1 elif album1 > album2: return 1 return sort_songs_by_track(model, iter1, iter2, data) def sort_songs_by_artist(model, iter1, iter2, data=None): """Custom function to sort songs by artist, if the artists are the same it will sort by albums.""" artist1 = model[iter1][2] artist2 = model[iter2][2] if artist1 < artist2: return -1 elif artist1 > artist2: return 1 return sort_songs_by_album(model, iter1, iter2, data) viridian-1.2/AmpacheTools/plugins/template.py0000644000175000017500000000277211511672746021230 0ustar charliecharlie#!/usr/bin/env python ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE # # Template plugin... this plugin prints some information about the current song # Dave Eddy def __init__(): """Return an instance of the class used by the plugin when __init__() is called""" return TemplatePlugin() class TemplatePlugin: def __init__(self): """Called before the plugin is asked to do anything. title, author, and description must be set for Viridian to read the plugin.""" self.title = "Template Plugin" self.author = "Dave Eddy " self.description = "Prints some information when the song changes" def on_song_change(self, song_dict): """Called when the song changes in Viridian. A dictionary with all of the songs information is passed in as 'song_dict'""" for k,v in song_dict.iteritems(): print "song_dict['%s'] = '%s'" % (k,v) viridian-1.2/AmpacheTools/plugins/pidgin_status.py0000644000175000017500000000402311511672746022261 0ustar charliecharlie#!/usr/bin/env python ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE # # Plugin for Viridian to set your pidgin status # to the currently playing song. # def __init__(): """Return an instance of the class used by the plugin when __init__() is called""" return PidginPlugin() class PidginPlugin: def __init__(self): """called before the plugin is asked to do anything""" self.title = "Pidgin Status" self.author = "Dave Eddy " self.description = "Sets the current playing song as your pidgin status." def on_song_change(self, song_dict): """Called when the song changes in Viridian. A dictionary with all of the songs information is passed in as 'song_dict'""" try: import dbus bus = dbus.SessionBus() obj = bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject") self.purple = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface") self.set_message('Now Playing :: ' + song_dict['song_title'] + ' by ' + song_dict['artist_name']) except: pass def set_message(self, message): # Get current status type (Available/Away/etc.) current = self.purple.PurpleSavedstatusGetType(self.purple.PurpleSavedstatusGetCurrent()) # Create new transient status and activate it status = self.purple.PurpleSavedstatusNew("", current) self.purple.PurpleSavedstatusSetMessage(status, message) self.purple.PurpleSavedstatusActivate(status) viridian-1.2/AmpacheTools/AudioEngine.py0000755000175000017500000002326311511672746020124 0ustar charliecharlie#!/usr/bin/env python # -*- coding: utf-8 -*- ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE import pygst import gst import time import random class AudioEngine: """The class that controls playing the media from Ampache.""" def __init__(self, ampache_conn): """To construct an AudioEngine object you need to pass it an AmpacheSession object.""" ################################## # Variables ################################## self.ampache_conn = ampache_conn self.ampache_gui = None self.repeat_songs = False self.shuffle_songs = False self.songs_list = [] self.song_num = -1 # create a playbin (plays media form an uri) self.player = gst.element_factory_make("playbin2", "player") # source = gst.element_factory_make("souphttpsrc", "source") # source.set_property('user-agent', 'Viridian 1.0 (http://viridian.daveeddy.com)') # self.player.add(source) bus = self.player.get_bus() bus.add_signal_watch() bus.enable_sync_message_emission() bus.connect("message", self.on_message) self.player.connect("about-to-finish", self.on_about_to_finish) def set_ampache_gui_hook(self, ampache_gui): """Attach the GUI to this object, so the audio_engine can alert the GUI of song changes""" self.ampache_gui = ampache_gui def play_from_list_of_songs(self, songs_list, song_num=0): """Takes a list of song_ids and position in the list and plays it. This function will use the AmpacheSession to turn song_ids into song_urls.""" if not songs_list: print "Can't play empty list" return False self.songs_list = songs_list self.song_num = song_num try: # get the song_url and play it song_url = self.ampache_conn.get_song_url( self.songs_list[self.song_num] ) self.player.set_state(gst.STATE_NULL) self.player.set_property('uri', song_url) self.player.set_state(gst.STATE_PLAYING) if self.ampache_gui != None: self.ampache_gui.audioengine_song_changed(songs_list[song_num]) # hook into GUI except: # out of songs self.stop() if self.ampache_gui != None: self.ampache_gui.audioengine_song_changed(None) # hook into GUI print "No more songs" def on_message(self, bus, message): """This function runs when the player gets a message (event). This allows the GUI to determine when it reaches the end of a song.""" t = message.type if t == gst.MESSAGE_EOS: # end of song self.stop() print "Song is over -- trying next song" self.next_track(True) elif t == gst.MESSAGE_ERROR: # error! self.stop() err, debug = message.parse_error() result = "Gstreamer Error: %s %s" % (err, debug) print result if self.ampache_gui != None: self.ampache_gui.audioengine_error_callback(result) def on_about_to_finish(self, player): #self.next_track_gapless() return def query_position(self): """Returns position in nanoseconds""" try: position, format = self.player.query_position(gst.FORMAT_TIME) except: position = -1 #try: # duration, format = self.player.query_duration(gst.FORMAT_TIME) #except: # duration = gst.CLOCK_TIME_NONE return position def get_state(self, *args): """Returns a string that tells the current state of the player.""" state = self.player.get_state() for current_state in state: current_state = str(current_state) try: if current_state == str(gst.STATE_PLAYING): return "playing" elif current_state == str(gst.STATE_PAUSED): return "paused" elif current_state == str(gst.STATE_NULL): return "stopped" except: pass return None def set_playlist(self, list): """Sets the current playlist to list.""" self.songs_list = list def get_playlist(self, *args): """Returns the current playlist in a list of song_ids.""" return self.songs_list def set_current_song(self, song_num, *args): """Sets the current song num (doesn't affect what is currently playing).""" self.song_num = song_num def get_current_song(self, *args): """Returns the current playing songs position in the list.""" return self.song_num def get_current_song_id(self, *args): """Returns the current playing song_id or None.""" if self.song_num == -1: return None return self.songs_list[self.song_num] def set_repeat_songs(self, value, *args): # must be True or False """Set songs to repeat. Takes True or False.""" self.repeat_songs = value def get_repeat_songs(self, *args): """True if songs are set to repeat.""" return self.repeat_songs def set_shuffle_songs(self, value, *args): """Set songs to shuffle. Takes True or False.""" self.shuffle_songs = value def get_shuffle_songs(self, value, *args): """True if songs are set to shuffle.""" return self.shuffle_songs def set_volume(self, percent, *args): """Sets the volume, must be 0-100.""" if percent <= 0: volume = 0 elif percent >= 100: volume = 1 else: volume = percent / 100.0 self.player.set_property('volume', float(volume)) return True def get_volume(self, *args): """Gets the volume.""" return self.player.get_property('volume')*100 def clear_playlist(self, *args): """Clear the current playlist and stop the song.""" self.stop() self.songs_list = [] self.song_num = -1 if self.ampache_gui != None: self.ampache_gui.audioengine_song_changed(None) def seek(self, seek_time_secs): """Seek function, doesn't work on some distros.""" return self.player.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, int(seek_time_secs) * gst.SECOND) def stop(self, *args): """Tells the player to stop.""" try: self.player.set_state(gst.STATE_NULL) except: return False return True def pause(self, *args): """Tells the player to pause.""" try: self.player.set_state(gst.STATE_PAUSED) except: return False return True def play(self, *args): """Tells the player to play.""" try: self.player.set_state(gst.STATE_PLAYING) except: return False return True def restart(self, *args): """Tells tho player to restart the song if it is playing.""" if self.get_state() == "playing": self.play_from_list_of_songs(self.songs_list, self.song_num) return True return False def change_song(self, song_num, *args): """Change song to the given song number.""" self.play_from_list_of_songs(self.songs_list, song_num) return True def remove_from_playlist(self, song_id, *args): """Remove the song_id from the playlist.""" try: song_num = self.songs_list.index(song_id) if song_num <= self.song_num: self.song_num -= 1 self.songs_list.remove(song_id) except: return False return True def insert_into_playlist(self, song_id, song_num=None): """insert the song_id into the playlist, song_num is optional.""" if song_num == None: self.songs_list.append(song_id) else: self.songs_list.insert(song_num, song_id) def prev_track(self, *args): """Tells the player to go back a song in the playlist. This function takes care of repeating songs if enabled.""" self.song_num -= 1 if self.repeat_songs: # if the user wants the album to repeat self.song_num = (self.song_num + len(self.songs_list)) % len(self.songs_list) # this is for repeating tracks else: # the user doesn't want the album to repeat if self.song_num < 0: self.song_num = 0 return False self.play_from_list_of_songs(self.songs_list, self.song_num) return True def next_track(self, auto=False): """Tells the player to go forward a song in the playlist. This function takes care of repeating songs if enabled.""" if self.shuffle_songs: new_song_num = int( random.random() * len(self.songs_list) ) - 1 if new_song_num == self.song_num: self.song_num = None else: self.song_num = new_song_num if self.song_num == None: # the user clicked prev too many times self.song_num = 0 else: self.song_num += 1 if self.repeat_songs: # if the user wants the album to repeat self.song_num = self.song_num % len(self.songs_list) else: # don't repeat if self.song_num >= len(self.songs_list): # dont' let the current position go over the playlist length if auto: self.song_num = -1 self.stop() if self.ampache_gui != None: self.ampache_gui.audioengine_song_changed(None) return True else: self.song_num = len(self.songs_list) - 1 return False self.play_from_list_of_songs(self.songs_list, self.song_num) return True #def next_track_gapless(self): #"""Tell the player to play the next song right away.""" #try: #if self.song_num == None: # the user clicked prev too many times #self.song_num = 0 #else: #self.song_num += 1 #if self.repeat_songs: # if the user wants the album to repeat #self.song_num = self.song_num % len(self.songs_list) #else: # don't repeat #if self.song_num >= len(self.songs_list): ## dont' let the current position go over the playlist length #self.song_num = -1 #self.stop() #return #print "New song_num", self.song_num #self.player.set_property('uri', self.ampache_conn.get_song_url(self.songs_list[self.song_num])) #self.ampache_gui.audioengine_song_changed(songs_list[song_num]) #except: #return False #return True viridian-1.2/AmpacheTools/images/star_rating_gold.png0000644000175000017500000000070411511672746022650 0ustar charliecharliePNG  IHDRaIDAT8?KQwgJ0"bXXB*U$!m:`"MPlDmPVja Yv8qֽv]8ͻ{x vYgs ~S% |;ݶ)rMŔT\ .6yL  W[8N6]tv}+ Jp^ώ1Ycvc2nW<ƾkJkc^! Dz\`{Z[ y/`㸂HbAezCđJ%Ob.WB+!~7YEbhxCN5ݘqm~KZ++{hG>g-t{tIENDB`viridian-1.2/AmpacheTools/images/empty.png0000644000175000017500000000010611511672746020460 0ustar charliecharliePNG  IHDRĉ IDATc?ͫ4IENDB`viridian-1.2/AmpacheTools/images/play.png0000644000175000017500000005307211511672746020301 0ustar charliecharliePNG  IHDRx pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FKeIDATxylG}Suwߎf- cX110&XlwH`v IHjwi 2X Faƃ1fne6ikOzO[ouNU̳r頻'[4*\ 0 `0 `0 `0 ` `0 `00 `0 ``0PjrPG0Ʌ..uB O썈$?rnDļonXkHs @D.I=zn[]|?-`EN /}%<笳z_[[{F}1fa1>?8mgCWUɓ_>q>Gҗxy2hP.++>x3i}:[wX$Y Zk[kSرc{Ȼ{Dd+`f-,p?OwEd/sܟ۷oϮ<%&~.U NIcM`ʞ[eBSN}ȑ#x]w?33eZg2 2w! /{^uƓյ6ƈ=|0_CRD/DApbϙL}h7o>!"nV5F-,p#\W_җs%I ߻.qXD]"& ΏIJ{}_=*)V  O%\rK^~Ku ^t@Qe6 esn_fؤC={nxIqk!qxի^uO>͝OK\ ;Z-q̄QLeW@K5bFxqGB:}?W9 #a0_ܷ*"_+=ܷˢgkj.(\llNɓ_׿~M7F@eE4neY{^%Ub,ܾˡtbv:(Ƙ >V( ̀=zos#N6 l ` oUDvmo{v:t=n}L(<gLc!W-Zz{=yϟeـ| `L,/Cx+~k ?E$I2,`q'd˄} k2#`yo'> ,` +"~e߻]9$ߝ$1UĪ/IȢ"7| ܞ=nhB;4ǥbAwAAz YZLLRRXwT_> ZwwDdMDv]wu9m$kM˱P*_w1lXTE.6X?d zs=o[?+"'d8%08g7o{ۮO~3‡wqOUu( v#F;`{ƛB/4>yyw|}ƘB&xM }o#"r\f.0}loFW-r#U;3U[+&IRx=wP,{_Nw9&505g} o|Mwɾ@GG}.SkŪJ2(ԉ58~K:m `{Ɲ@⟏SX 㘀ĘTw:| oLi Ww=Y:?hlnG 'Y_kp'dbـ[ (-}'m{0d Yg=oxÇwu)qyn]wE>N :m `>nD[1d{{ A6J'I #ҿ^1fcՑGowm0b9]{9p[xݻ/qٌG_Ӫn tAmpD־Pn+,>5;9l5Y|3_0`HL"&1C3_l374Mc` @aks߾},sko!e4j^-lsFh4~ x|p'q&Yoic{tm}3cod<E~_g\U*۪!FӾDhrbJ&ϏGY.ddyohL1PV}ye0p:h-_K?q[ځ+7y'p\j )Qu27#ѽFM}#sَ<3xkFvD^'_+ӟ&08qȏ~,$ 5L hOjӁϣ TF@qe N#zWMb|Fǧ P4(.׶No};2,x=ZhWEdOhmuR1gw~܌@h!l FPr$(J]]wıcnJD~Ƌ0m4}ox޹wWH܍y|{^o`?&zF`V v?MG7r8;ľ XMDBy` ` @o_˞ԧZaKNs _6sC'M<@I0n?/;B8_n m ,nvҨ/~q.N. `=?._.tIɍDޱ?[d㗝8N$3^`~}0̂4bG̃sYUɓw}z-9adKcᄑ?M;"}ް @}MǪ$IXӽN~iiv4EFl%٪7x3XX 1c͈08s̅?Fw)L|1ƈMH$?rJLNr_ ."q`޽?:%{\X0N w__O)S7&Aaq20kשѴx}5V6< #"B[02s=r_,]_.^/ I qfQFy_'^aƠh6`JaR\ { U珁+?g<7ezd0m4y៍}2MӨ-p -H M[0ClvWqHc?V}2 :9s^o|^0A៧\t:KX `-+E5 Lw!KjYQ g8}{f){{1#k%"N`DoС^iӴ ։~trZ^Js?7)>)EZL # lK_wـ]hp=N=w`^Pc>ǿ*ӭD h "kbVlj aoPd# v𜾓jwߠsoW(ΞX;?ܚ}uLD=/^J:z qZTD>  =#"^/ #y fBo/!-jdj+F2ałFUMLj!ɺx@=Wp)upmE7?vlݒMʀؙ 3͢}O{=Q)N`Gſ ta:p3ؿkp"(fitV@,¸l;SՑ#S_ /^^p^g/OfmG3;IOW-αkv>v XS+ڰ7Wi@GԲ>}u|g^Jj܆F<8$amujЂ O9R^E~Y`Qӟl ,|_n5$A/`@qg:0,6y(&̫v̰Ϫox?91 =~D<3yT h駣VwÅNDZ pP0@hflXȾ3mf&soc $~ݭ@_,k_?gͿu `v,dsWt^!]VT$VX?OeDͬVt _lVIin'LN>5Ou8 P=i𯯭c @Ƅt:Jh.|TN䵃Ө6mﳑduJcEDVVϒ?pYg:& v@P LF^/pa B|̈́B ʰA ٦*ޛgџ54w!P7Q$ILapTTݫi-h$DU3EΘmssp6#"+++Ou۟ b4oG6 (#L`vi,nSصot;M̀(1;DM7@?g*}7¿\@6FD0,@n7 \NV И ReQHCqoO 'ޮu?!eat;VmS ?%}՛ϧg $U'E`0κyS鯑d}WamЎ_܃/ t*WV 9MaAbt}p=![) fa\ QqE|Z,:Y4dvMS5)3~֒) WqЪ. gk&>U0S,ڔ OK+L`1ѕ}#S NYb?#(ZI2;d`@a3uNiӑU,ך wo'55pd߿ugWV+0`2Y4@Ѥ̺N yem?0mK꺃<- lOja$AoKHƀ9 @K?G/^`~҆诡?N0m))G#̇ ’) M8hO#( {V ;ruRԿ9+%J/I~uf9o@*NQ!mꣳsFKlG @[*'mX (Yp)!Uzlx@~/ǎ=&OL%37GԿ Gr!/`z)bDw޽[ԧۯɱcF"L2oLo `c5?T`p/SVZvj@݁ǎĿww^93墍 寿EyLJkK &2p_>%G=?p d&[|z'z @ GWy;mLȮ]dϞ=&WO|L9!9Y&Lrƅ#ur `qc U.>|A|l[YY5ٽ{lllH$gr c׵Ƙ %? 3XNuc$Iv*kkk "r/\j>(&`j&$s1RF @`:`Q1FaWk.{Mf.DNvʔ̍m`|et`8n;&C&85qПÀXhYւPȰЈOL/ ^#6L]{p5m|r .׾s8NBL `ZڤkV 0؆%w: n^+~,X@ѼXWc .jimscL-׼Vnrᇋ;vċO䭙EW~| سg߿_<ɓ8#4<nD'nO<5#" -*>A @@(hW@K-X)ͺ&:[dT Vm-Z669Bh^Y[o^/|-'C}!X6`Z٦}5_50X&൯zl> %XG$1-͕R0F)o?1ox}-kۚ^ Gth>ft6Suo蛀Û-رv_zlʀ1fW,T;,o$ib~M@{2K,  @-345ozwo@Ȳ0YG46@2ubIJ %F'SZ\ooy T:ڽEEbETh_ \7/ he56P_'bf7Yro"kehff~ꛀ&c PO#YkޘMLE0ul41ϹͿ l=ǷP'mmf - 6C+3Ls7].7;0bgV7P]m xބ m;* f y7kpSvX @m"`!=&+ƛS4 mpՆV+#bnQ{~H46W^C^@ܮ'zWmfgLvJ 馛D2 ȍ@ӑO|Ī D~*wy_/0V4}C/~%MSc.ϯ 2u?` @0? s2H,p~_?q` ` @0? `G0@?` ` @0#@ 0@?` ` ?@?` 0@?`?0@?` 0 @0?` @?,] .`Q0"%0S)w&ߎ`qiw! 0~w#\L` ^u0""Ⱦ}GĪ"$ 0:, 0FVWW%MS?O{ $FzBNQk~*eh_җS|X,'@,f =,^A`a Ə?9ئL+X<7KCLL R6|3X&K `h08xhiU4@ic|4dՓ樵Z8-p54e|fhAUU8Rm"cX6qo06c1`vĜRZj^X?b{BD,&lEĪD7 7?`GLʑ-v K%_֚P=_ E$U&vj*_3 &oy86' ,`GZqYfVM+)l'Wy0ޖoW桔,FcV#'"=k"rB&md`)rs0Hly1.҇1HxZU3\>eTTM)-v5Lo~' De ߊle 5ok^8ϰM`[t`KDN6nZPeF:rOާG3cڐ2T{1 0 ~]DN"@V+%>a >1_OJq `v9i|Yg& ):5i`ƭgAUQ[kdL/J<odh+enNyQ_e3pJk`Z @K?'"tEF~4`? ܅U􈺋íU -J?hgEcAЊ @> pBDy-obmNGǭM1,Y @exf*,Ɩ?1VT  @ȧNe qk)wcN`u?-m'd?u pBD韋&`̳00_xHV@~NK;$6w5E w,7U/"rϢ1s85GoZkHU 3U+sziپDz?`'% "GUIM@=Qa :5Wg~91n#8*"4ҊSoJ0DZ_ѭayD  8e#گYkj%&ɳ9x֪7-MTDEG1K{~5'nld0P4]fkgR+)~OZGD^>w`NFJ9 ј[ܛT/HSpYQ4-sR Pڔ%h_=z_}~F"^ @+1& /X[k&Me=Δ@S#` DHQWU>HmW-& oT@WDVDd%1$y@z)^>NDĨhvg𻴩!]ZS^{UwߒxL`F"_e<""t%ILE>q#`>@+eOI]jO4I?W#aSd&K{5ݵ$/4T#L{FhUw+~v;SǶF @"vu.&;0ޝN7Mǣz_ N R9%M<)?McM ҟ=HD~㽴?13 b?g" 8?0$"饽v{,Iـ(>CWf*3pV/?? %"#k]׊_|1/!LfL}Neta`>NO=N6W `Q޻E'xn_Dx/aw@‰M6ڀ`1Z_4rnNe&`5tSv̌ـ(CPL4#ug<}UFb|0,k ldр9Z*#lԸWE{\2kWz{sT+@3mZXpq E~>]%~>lзT |&c*q (Ō!׻ٿ5#"D䠈—(MmWi%tÁ-FDߍ~Z Ѐ(Z-f.GMܽ? w_lߓ23pt~(D,zR+KOw=aT}`F@b_65`>` $$r]ocwۊ7Ɯtgc̓ɜ`n}^XM{韩dgp~ߩL{^T^U2ZaG ˆ&S3܎=Hr:ʦ L;O 7~9lYk&&9tHLrƸ>\3>E2?Dyq'=oL_$<}y&z`fbp~"C*ù^vߖUe{ac̓:I\ikj~_UsD>|k_ߟ M0|} g(09?det\nh / ̈́_1{'Dd#j$g%&9$R@ eLmzTs?I_.yx/`}#* KhD|+yP K4`-ݖ/9\9YS!8)턻$$I11f?;z[Qk3~r\-}'q'>_(R\ X j_Dy`~][/V "C`u+ٿZo&-cƘ]Ƙ!؝a`!IUd&Zkdb~.ܭ}-)f'ݟmZP+=/{ RBoK -}*.oh:_|_~WF淭BuWn#\qL k 9]00Ƙ]FFf6_~_ƘuR`U+jzRDzzҪ}BU2y~-.;i'+|'~\q?투k B]3heܯ[D(Vl `A @貔n( DAD(X_q"bB~f`:vs2H:?utIn)<~QVꌟ3x y_eU)R -яsv!ʄ ױ bѾks"xW{>~:"~2q^5 Hf#j]ב@'9?SOx |-rs]Fxۚ:q;k0m#Q4A1n+_k>&&b)'2wD6uD<{i'S%ŴW SBѾ+"R2ip @H)3"b@IDTV AgS @Wk Rj0 M@J""1RPTef<#: oІWL>[!hED]QpL ]U ϯrn˅:PZ_#_nb[M'|Rw,3uj>\v0׻}+AlqLH\S߿*%^"toFHyuEPA" @ 0 cPw=NEŸGEu w(-\vȌ[ ŭϕŻbKR􀟩ϼHMѷG,c~8`hpLl=!) ?Ο #}as?%16H AL4>_KGM<:^G"q6櫢2p% yS>LCs}[㒿-ﻶO7u5MI/ A2)y ]cЍE'`+ 躯.w%cWEHd_fLL"^vY`"r7 wFn'"ݒȽPj۔ӉD♍Pt93ķj@Te*57j~=啽[a "m%"2/EE~6ZV*5~4.CP6e {LBȄi#3U+=y-w`ux:YND$"!3cMĘ$̅hyOI>s}HF=6d,${PV`R#D8IS#3Pfc7,6u-HV TJW"} L/w|y+{v[؊,m uDʘ`T M^i 96 4쮌N HEdnݳ @H1Ӎ|$ɗ:?CTseULW w"#Sa4m8{: KZV)KeWM#؀zll\ UKx^mE^U؆>`&|~HClZP$%"ԩyD,+!N嗙䄦7B"s!Ee)˶4F"2Z'LTމ D9{lhi8βҀj z/`c*yZ!e eq{cަQ$'0qxNB똟T/Kjd9LوeKLn(ԈMq)3Zߦ 0icZĻĈQz;U[2u#qVmv"lI#I40 UH<_'!}U yUo&iM׬D\ܺ﹉T==* 눬ȎT2!MkDt! 50Y5U U\ATO_%L{uwU8+z;4xN]ö]}g}OEZV? ~/fISH utm祈ƾK3vL+K0逞4|dO^;O 1m4ܔA}4VDM͌S` Mi\V1y5 }6Cc~T3W)Ȥ5?'i[!` "ժ 3-Sb00bvLoYG5#eٯHX&} $1ف2h[x޸ݎU[R0`05 my4#u(8daQ``^}Y2sI'{c`0.`, ` `0 `0-`0 `0  `0 `00 `0 ``?eSpNIENDB`viridian-1.2/AmpacheTools/images/127167-simple-black-square-icon-media-a-media28-stop.png0000644000175000017500000005071211511672746030373 0ustar charliecharliePNG  IHDRx pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FFIDATx{lY]k>{νwe; 2N2R<b PH*P,QB(/&"f3a09޻{ڏ~SunwݽZoQUÅA @ @  @ @@ @  @ @Ƌf̡^s(ul0a7&<kB[@Lg썈${?1ykm0$Z#"Rܮ@蝑KD۳gϞ{_)|s_zA-C onؾ替ԩSO| 1laZkȲk/_Kt/9s׾PD:K+e@??x7'N|G~Ai>}545Ά>WU˟t?{{__ Lb[@y5\Oxk^y> $byQZW.\;ox >l0 ae/{?~kkkid>okX6v>3gμ[ob`ϋ N`Q|cOO>yѕ7ֿ#f3:JO0 'YDENg.^}}ܨ@o p~7t+N<$Ioy\o?* " Ώ Yp8ӧr-~VS2a~MDokg'Nokڀk!1h{0pC\׉9?vo!!`oo}[/~\Jyi!X7?IO:ǯ|&MңƘQ_FԱOq GL4J_V_8;VX}3{W7 ۽{ٟEDv?>-܀N `.TD6D+_z#sIlt/C¦~5cQݺkH@1h8ww7~7>""d2%8XfPAn_WW=nj 8%> Wձ/`<G$ƌǥc˿ ؙ`A,p'< W/xSc2&Sn@S 'DbрZ [-|w/Oy2 ?uK_#GhjT{[W]^i={^`|xPі>*z3LٻG9x*iFda*ƪ3gϾW^`A,p=z_loo&T[xq3J+n2@- y[8q4G$?aH,L2'Iv"Ɇ8s׽ oy2RŸ_>~? B=#8WKs~91:@/Ǥz7*y-M Lr-ưWDK}E_+GZ=@?_u[-D]~ۗh\:M >BIz5ޮB<(@VD&"#mj/坷G&S !."?G^믿*9`/ļbZ;V^MZE tMŻ/yX_4̙oco5E?7"uc6pcQeA_jw󎊥E[%ܼ.7M?""ʨlp@!@@1&"GDK;vK U'zżU@ZLƲ~y"6*J#K-}s_G=꿔:7G"5`$?ψH $6%43pyE"/,dtF}1nOzǥ@,pCGqn70I5;ǾCtT7p;~Nx2Su9V/X#4@R`E<8UU._|ۯן/"ge'-e> 7%"W/cǏ@4 gJtZIdkJ*wrgGS <@}"Q$o{{R._ xW-{Ͻ}|`@n?Ytع޿o!4taG*G3)(rJo\p/ĔwtǽPf?g("AQlYw\w_k2QS?׶#cFmw2UM\PG~쩚jMBalgqZ@c٬W{9JE4#z=v 7_h oD"Px["r}Scӎ\q9&yz50-cRw"n>w4-ErBg9'UA`5ټkq6k2.\]r~1&?N xa8-})mmJsS{r:tgEVv,qNEʨ0ܶ Ie)OyʿVcZbs kK|_=B_(4 &2eeZcfWcsQ.* b<|Rw Ǿ."")O{Ӟ}r0>wꫯc/-g-&NB`Ŷ~]7bĊu"9_/nעLgdtEv4Z㿹y:/U[OK DuRP5Ŵq2~5/?LO ome*m?'/1;j5pѻ̤pYWc'FvN[th@]DW1Flf+1FLQ`(1yh}m'?7Tf-Dd}kkc^9%{-je.,'㝶]WEߴ(9HPGD}E៭Ǐ??˲-8@ϥU2@COv?a@,O8彷ƱO* ɵ^/ q<ߒ2'VjukCy;F"8kx~eز0|p2K0XD.AqRP1bB`Du2$h@"oO=f( ǾwRZQJs>7))1s(Ƌr^}YʵG4&bZ7Uwʀ:Ÿ7o}~su=a'"G6ojfl5Ԏ3R^ 3Y&;Ju\qa" `gNmW+eYe1XS+ڤ7W i[+Efu/ OJj&BY<ޛ$4-^DX A`G\Ul⯳WѱouTXԌK*_[h #I8O2@XD@IMoM+gUMv ō[V|T6 sY'|?UW]uգN>}VX QGRqm{޿',>[.  6)nb<>V:!j%%/bUbQN>7y`0xb&1^xHս"`dїHbl$XR`p):3 \ M8t`J+?"o*Q;.R9/uUW=cpi.RY[[_{mD@Ly[/l\Pz"٣Q+Y.#* yަ=?MT4FDd7>}6,}G 4}xG Z[)y7]h69U(I|vƿId`B ^$Iz[ȭ2uQXd Ix*b#HBXg1x 6KיHėMVs RNd p` 59+J*ִc\[FR'O)j>~@NϽ}EiM|J"$MD8 0Gp-mk$hPJr ?P=vb^5*Ffo#cPٿy%ԜϹ(H98`4I5cmUËOB~I@z@S1\1pX=BbQiBm/.o9<^2O0FDQM|8(LnC@Α9wm `ڍ3<MdnT?_]KQ꽐Yr8dP iI)IHiG,3E'-Ej P0&"a1ܰv*0ݺj*=bG/n`k@5э= twL TZQ1GԀo wcݾW(OT*QЋ6wX ~M_x -:210]_Zn~mc'͡Y`unS'$΍nwr:+};}5+1 0Uo~0Jo `7^, uRE^W)6/~%0ʕMB~v~Fz1\Ѓdk6b#  8 !Jv~Z#>KGm@"4,75Mեx}Xk4\8W]#ZrMb`^X)0&V=ck׫s)ȹpƿV9%,20`0GŃ觕s [Mg?:+}0WѲꏶB/~궃2% OxI[P0wepo?CE)зSe M*@h:4@:xCQѶڿo0Oۄ>+j />;p`}꣋s%+KQW_j}Hn/ ЩoFzs?f5kN[ IUsv Kl`dП)2"=~m5 0~)@/%A#߃@w 0cf0JZ>@tP+%899,Vs]"LqSnJܬ',&`腃$YDP%dkkf:O}ݎ^9@Ry?ZCh%"0މJ5@ޏT,'00 z|<0D`YҦ\btD@_L4}F@?=fO.omո@ ʜL.Pn7  VۆWC,\hZe0H$:!'k??suo5/TS- [uvF믗kF677e}}] IK!2%I26Ř rៈ@TU$59r=zT9".i#}nP$IRQ^h P.\U0'UؐGʉ'ѣ=;ZۓI$oNS7Ǐ)/FuY[[-9vXlmm}rEIҴ<=c2ʌ5mD g [K?kkk!r19~loo&zOe3XIB溄s\K@\X9]єJ$.\G 4]7P#Ix'C}?Z=mҁD UE4`0)Fà c C1Ǟ}DZ@Wocf P㻓 Fc͗Q!5bM>kFwƛ̐J @f`ҟ3PU|`nv#Z@_Z-MpRZۦMyz~Mi)V}ӦYX69F(9)X16^X/r Z"zdRs`GڴgΣǺ>{9]C¥qlʀgUk;k‡U@<>Mj nM7{m#_ #tYBO{N|aڶbJSH~џX @=ױzqyݱЇVX@?;%U _C' @\m:pu`"y_U""XסM %9 fz1O kh7FϛE{{\#X=OnlĵDUb.&0QS询W +ta[7mz!z-hyP1;_G%۶^[渆\J@?[>=98\cc\K2e:ylWQב>aU[vѦx]g`$\"0e@2WT?s{qE`"ZP_9B/cx DVІ$iIkFbm#qsb"@`lE:N}+ XMKۏGDma ` ><ȿ{C2F@)у4ג -L̉PR? L*kMq@pt 00'q5T"$XLkڴQ*Dy1+g:2_ ֮iӁR3Nj͛D@0*ydSIv-iXv]ijKH 3Ԧ_בket ג @  uEh]*k\7XvMn?vN @@@yQS5c1\XvMz= #T7T5:*i2n @X&K@ h08xiU,@iS|;H57 Yr9jN T@Mx>4_Qcf% >UՋE]?q06jX *"V֟ ŏ΋# [*z`"AG8ЦcrkCK!bD/"7mgո_Q5mFz_Jas@@H+.Ciլ۪iҦ whUe;q(% xU0Z{\Y[#Q88\UVD VcBdyCUջ1eTTM]Q't?Dfk]_oDd7wIDFM+K.O3(D E9{"vk$!hjVճ##*]jXv?*TRM"rɪ~Fv z7e)ek43aXNs3"r6˲OЗ6n=5UoXR 0W[k?!m׋@Qp|"rѪ~4$ K'I XՏ`G&9D f&"am:eR "k'_?PUz?UɀBᶪ jqڏG?"b\ȬV6`U}8:6G *xf*Vǒ?5V/U\ @(vzZS6_Dey ]wW\,SFrD,k?zUGGEfP/\%5mED6}9'+c_(^C,ע!3~VD"?^V+3YfW5qvwEAES]kP"Pfgۊi80X_ν2?@7,EbsWH tD1i^_D E䬌E_RE. A5i4_"`e(?/#1 pY"rf/_E>h F2?_>{d?`ȼ(Y9=̲TaFXyM +?j2Gew2 #T8DN8+"Xk?cҪ5׈yRDrRmUo^_D4˲Vկ#VE0oU='.$Pt1u݋ ⰵÙ"_Sd |c ~QNQ#%Zhʥ~!U0GFeȽ!V. P3zOeo4GT6%0ybxۜ(""r1E?48CyCZ,YzL)B @w=v {3 -FF`_D@Zf?3ƜL Mݦ :fQҦODQo giK [Cf&!^L5Yf?x$MHFB حt~K\ԧϲ,{( )g#:a8 \ {fi"@рf!0y(m g_d]cKL4Q7O?i8}W "`8| dI|OD4D4e2ܟ e4-;HŰD~c?g"NJ\lt>lށ .$Į6W"0с87̆c+/ݕI_"RAv6|\R`6`j+nBK510f^N=M4Q`UwDgxo{_D0bCF+ M>ڀvp1_t0IB"`=?d6^`h@Y (?ef0O:;EDޕ{g\6R5p =Yk6`Zoc<.t=ZD@zPuz!˲Yk?﬌V=]IL&}`B@ƾnjz_[5w?f}q0ޱ5#0\?HllJ4}X_Άٟi<:bw=޽?l01! h?+c47H^ {}.ͽԉlȱ\4M+M'ICG+#ڒcVJ/Q>n|n:e}_dNd @$""Bs"sI@($ޭ7αy #8.cƘ4$ 2}6˲O(7+a<Ww  H4@J U[`Z0B޼Hu0)OG 絪j$IOIƘm:#V6,nS2 w y%[MJu:s -dHxT,ӿٮ0`ChHQ/ |9>pzT0Zkl=$ui>NԵE.ZAf6s.2Iv7n] ~PmMw~c/:A!rYk_OLru&MLr崃>(@_ȍ/ =?DEsW/:PD'l@`!b旀 M 3{gLΆs{Voc&u&1y7s^j~UUr|a/揋}E?' M0\nR].ZB} 1tK"lxF2YOTb$RmhtzYkOg6I~a/:WPB?^},FHJ `.6O 1/Jy^>TU" 5X?EcEb̎xDvrApYF Xkw{%$I11fvC<詞jRճxw]vƝ㜸K[p_g%<%Q ش wZe" f?ح>^rqw:x_❧-Xۍoېђcc̉\l aXXI2Ugrc;0ܥ}ߞ0p,T WXREZ'ZX[Bhp9"`]&6c}W41ѥ5Ged, VUw]Ed-]$o3NT`/`T}1(Ut@:j_PO]`E@OMEkc~4"R/:y`{~yL u/jMq2oCH$AR~ۦ6:y,e_J]{uЖ^[wlHb(7]joaQ"y9D{@]c=xY75p~:늄A$2JZ@5B9g܎[Wy_ |KrlXBǺ 6i^h0o!Ѧ< ,]8V|MvOw 1?t ' vH~x_%w yA E$E+"8|@$\ (0E|>4E0"~Da F <0rn@S A_ĮW1Io,-ESka4{ކy x6`B-- ^HҷQ3U ϯs:7F<}B ĖY o~oC?Y:J+`@.m:(nא2Z$r0IAyŴ[ym6PXn;mr [+=q׋w?C|zT'^ѷo01XQ-1cQ 4>n7J~NAh!uTQ~G$ƦTČw"LKk&zԥ.0hc6:O~f&N<\ حc"{CG39zp)۞wĶuF$ `u@ Լƴή0D E FMx$ |A?xuDoc׿u37m4 :YVѸw8hi|[P]oՆ}S?yu;e5+Vkz L!hDPĆm.w3 OX E][vXAPmi`O=[} 5F;V ā7Cmm,c?aI;Oj yl%BlFOE2JuuDHWvocmw֚6y@o@7"EMdHCVU,3Zދ#XB~0B?.unnI5F gyN%>,0OyNӀߛFsO#F>oV_I#޺xb#'5b1۔#tMJsph_yyu6ik Lbhz?Fe%<q )aDŽOH$DFQ5Ssc/yoD҈Qa[#&PڐA]݂Y =?:N3`[~M:4XH7fw(N#QP)] auXs} eX[ma@ncԬL `5@]CȱAa:% tF"! ['_'H܆i0̉U15p'>1moY4Y6 fX aSPv4 ƖՉPL;e "ÆĸhQO_n;5~6"="sCmǾWJ04t],KZ9wbp^7ym4:C1mWjny :YM5* ]nw%nui+:7钤e}cfZ6u [ZϜmyص4 hG@Is'S?wMgCdcކHwQ퍴t)l*f,ћ.ϽFXV8"g9u:nZ9K>OJ6g#2k!f֏x/!G VQ<զ,3/Qb00"^QVLۗubMT" ab;&]mjϮo1:F>e#޻*`q( ,ľٌY2{W@x@ @@ @ W@ @  @ @ @ @X]Mj{wIENDB`viridian-1.2/AmpacheTools/images/ViridianSimple.svg0000644000175000017500000000567511511672746022274 0ustar charliecharlie viridian-1.2/AmpacheTools/images/playing.png0000644000175000017500000000114211511672746020766 0ustar charliecharliePNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDAT8K`Ncc0V!s8ۘ%iŵY61ad!_ZoZ,Lax/=ͣ0/a\~1XƆ_yr6TZ۶rNJ-DZO-k H3d~ժ|VRyVms/X\x \DKPVGlja{{ fYީn$wZ:ky_tnl6aY0rs|蠷1^3_rl q`6" `OH58QD ٪Sj4֡( (DQD0%YB3 <~W(Xif M@ ؤ澞=3H<9rϲbk9x+J rolHl>9YN{rJ7*3h|Uo<>IENDB`viridian-1.2/AmpacheTools/images/pause.png0000644000175000017500000005265711511672746020461 0ustar charliecharliePNG  IHDRx pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FJIDATx{lYU>ν `E`FFS)1*5eX`&T EcJL1)-!&"&Iu!" ̝ǽWsٽyt>U׏w{}kmTU`0 @ @ @ @@ @  @ @@pP{ь?irPǖ!vxl"@ ~mP!{#"I}x1oxd0$Z#"Rp9#$?\؛?_2=-=y;|ы^7|{{{OݽcwwiƘ ;@,>sttׯ_ĵk>d? !FPo[\xf0<5;(UCq6*GGGw>]z=X@>lPm[o/pgg];0I"!fWeЪmǿss{E4 b[f"?׼5|ֳM.\pџRq -r 5S:1z\\{gwߟS/29!-Clp&W~W^z^p|1FL1f| Z0""@e@dz׮{/Iύ tF`$\0;/yKnҥKJ䆉ny~(z1?F6I4p~L0,UUTUFѧ._x;Ç>+R"!AE//~߻x7M ]p 8$bMFY0Tջ~p_cx??<"0B"oę}|߽74Ic!~R.5F1adGTTsE1S!`ϸ}uXNNO}9C`*/,vDdo7ԫ&kߏFWU^4BUNCG^ַQ. ! g)eMne/} _~pp:b,ܾCtPՍ1m >VrĀ^r'އh@v& ?oGD_o8&1c3U0 &)n31P+mkF?G?<P8﮻/%?7ұ ?D$I9$ VYNHW~US!P7B>Coyӛ/\hR0 6AGD^W}sd݉I➾8SEqObfo.yx>|Є4ǥIuPh޿~/~&&) )%~t}_y//'܀N `}|*""׾['$Ivcpa]Hc8۱jnU]km~$4{~~CDtJ`%"qgPAn_׽7=13*||*\U'㔖y?8Gh5}tJoJQ.-~8G1H@(8}oy[~VD k}߻p 7p7UPhf~UB ̬7BFgXϘSUi!PLt<7(=(gYf2[3ZPz뭗^ïm t>nZ+VUI/a}?x)wJSybpRRķ_0k#c&R1|ӿ\/[0 &\w? PbNȋo_jp ٌ?Mh@U_-׍TU<Ee@@|O~2s܃H/ax=T<(hK}hl) {|SQ( x'ws2-:[(wDҥKOWH $%ԉpyE"/,dtF}1s=٤@ p V'K?I>b_g |6|8f8]p `D&NgdkaSLhɦK"II<ƺ@gv=B/} @wï+^&;0E("2V$gdF@ߊ-uRHZXi+`/NF~IKU[k x]ӊ!_.i=30Qj*~`[\Xg@`Zcf/WcsQ.*V b|Rw Ǿ.""_"(?-r ɪfTS\8ΫMuE@FXNd@Pbvx-jyθ/t$EFXeoJ|~E5eRJc l3>=iu@lo@>Ӗ&g'|I~ormKܗ|,Pv9~IQ3[L{8f$ZN?GvΙhF+1Flfg(Ƥ"wHb.Z3('dGQ@g)ys^&(ۨkts/L=}/,&rKZF &EgWBaqyпЈ}3g<c((b v.\xEeQ_[p-Kgϔu6d -V Ģg_qYgck>INnc@gӾl5ݹX1i7Y;[WI /w![h_R *λxޭK&[B@Eht܋ eIE- e,X žKiN&~L9쯎^G^̒70ϊP<2Вil=j޻;9cȇ?rua'"{{{w[b3[Ml5N>3V^ 3]&M:Ym" `NmWgkyqR5~dUm#EB[\x%"|mU0 V'`i_/1FYpkk0 ʾ-:¸L𫑙]n?yaȂa~a#.XV 10Cwbz{<^\|^ Ã׮^{,oOef2 ޥKN iI#aI--;txuf&/o–Gv>a}V4{޺; :-'\B;9O/{%i@r<7\*h7otw2q&~ nwВGezDUyxN3S_(^^psx"Kx8TDv/^xs Zc|PXS+ڴ7W i(Eu/ OJjܦBY<ٛ$|/DX @`'Tl⯳Wщou\XԌ'K*_[h #I$O2@VXE@ۼIgM77ou+VgUMw ō[V|l@BuOf s~&n馧\|Rv)t\މ *2"ϖ BF0ذ!};eNLT>ʝNHZ_ XտX'/_ڼI`0<EJH/^"BE/#jVI\i<تtN6 >3qk6Ou: P?is". i7PD+ 豈igI:zMμF;uɿ:4},"2qq@曟nm zeK" 8}X_LznfP 04@͹|a)_CN־ yl*I䩹6ΊX %P<IL^ϔ |dK 81EYF˜bI[' 4FD 61ғU"v@Ur._!ছnz˗"\"2 ?keo 6(}_S<#أXQ+ĢsMU[<oӞ?NTFD^|2] i; _?ΜL"$MDXsZL8[5 (ie(xRG ~gGT:WteM Hٿ~%TϹ(H8`D@$ИPUfEuRVWpд t j.tQdslVXFn^f%@%czϗ^':{NE;) ྾7v@upI]!Lb>3W]͗G2+ϵu}9;R#IFv2stRUc"b hRۉ^J߭(ZMo&?G@7F4d0L;&[U.`*m+. wy:iV&IP/.V٧~\ U< $- 0wXdp4?s1ƈʹsPeoooZ+YM/t;wN&B:okF#999Ĥ]诡C{9?k]7?2@}]66FܹsrʼnݕpAYUe4w.Y*$nsMD`Z+rtt$f0-Ed+M*@h:@w:dYcx[@YZӟ*݉*DXU.24 . 7 kRYɉ\~]$ɓmĵblm4B \IaF5#/.p8=988Y899k׮p'(XٙL\paX8>>p).bhӛ1g@tZbF %_*mc е`09<<õ #@jiװx$ Cݕ}ߗC9885RF=L]p7]#yb6kN;Rܕ;]lzcrN~Mw  2eggGvwwewwW֚h,$IN>RX\]Y[0:!tRg5@&Un|Js y_?IIdbȊ Idk:ׯ7 d05ýtLe}%پ-_UoCZojwJh @zss_V>f c@tg`ֲ|}:Au9·X߇L;Խ=:)79~f;z@'Gf9 u2Jv<˓; 35''_ z@},lHO_s7: ױ}m@1MY5fUEOur] )ǽoyUdN{цLDB謬զ!:-nקzw5y~Ҙ`eASwYE֝5IW^rៈ@׃';Jo*طׁL9"@@O| F~"} ݊SڱH {ۦYcݹ_}ZA'ߡqbӟ̀5l-wl|OJ^>%iqo?y_է0*,)@LmgJ$ɀu5 gL;0uY !56bm{ \ȷ1X- hY[܊w ^ z w^"3q^E(M yv]ݼ :L:Щv-Z\ft}C}ܢUc^};ڴA@tIT~`a~YuC@#bN3u,)h?=ȻܶSj%CB@?@dR wwuq/fbϵ_" _s{ᶡ~p]Wf!ߵ@O֯q#MR5uUG$Av YgS@XH.h㫳R{woY܏}MBk3*Ċ =߷{zܧuWX_@Zuouܱ~%ʺҒ[ +҅6S|;v:  MbDX]_c讪e:n{I[by 9Q`|mbе@Y#@]GAOrQP 4*說ئH ^d?#S*xEns?o yˌ17Xn7 ?rChq;d&k8(CtUӣ z%5[w]'e:6`ϙJۯwܚ݌G t `I[3=m3 L.3U+5^[/\n8yR6e?<9B~5  D:]{- 1Ny!ړGzWr}/:zMM3qťww\;Ыv븕hŇ}D@`a2PqR$w)XuܠLYg:Baqh7"Wpۈ6*eJK4&}% @ `Ke7bJW~ @ `(֩s1Н=9Wlu>m @ĠಡJl }A @ 2g6"Gq ,EA3` 1GN ;eɉi**iMeʈH_\ã#1h4Z(E?DĬ˳E3ƈȞ\$p60lp .z]=i+]&mi^wذ&K@ h08xhUlua{c-(` Y(Q *G\<@HVը1 jV@Em?͸u髁k*"פRV GB}#݄X} `"AXkԉPykMK!M>X8_ z" v*9'_3 Xk7BV x(`-UnF%,'WEgޖT؏ơ(WzVC`$"#k"r]mD`+jq0 z9lc9l8Ug7Чati3EWTzh " ?lU_D$wIDtFA#K. Î(D μE9"rC"z( N-u?U{^)Ѕ(ix< fۋef{Q)9$@@gV:e ~0_1#k{<3aץe1Е6n=UXc S w[k/m׋@QV>U鑈\nNyV8i@'FoIXΙmGC*Gt7>n26Pj ƿeſJ_<7D# AЉ@1 p]Dfz#F*>##HDtNdy@{*xf*6_ƒ?=VU(1NQL 1kVsN sެme$"t@ڿຈ<>ʲ_W9 @@u_!Ywqy$/U9"ƿI^f "Iy?#(ޣ4F"U_TGU,?_ 5qNFgDGS]iPEfm*)Dlƪ \;tpW|ee2@U n52GY "rEQԢ/?WQy~Ī#kiP7_#`e?4?#8|5<|ޭ[T K#V`i/{ޛ{t?Wd^\eٿSQFXZyM gd=r."4p< pED~Z_QkM ڨ߲heQU?ؔ~n<lUC>ty`+jWQL O[kߓw/ *\DlU~G-9 P7_\>4F?'?U(DêzeoGT6%0yb`K<}vOkne"r1E? :EqG,{_cL `C ϵs___F?{81(~10 31R$/qhB6_1*j5.mHt0ޡ.mh[Q6 T|/HPDvF dp!Ii'$n #nd~!VއZ٪/SQ׿gYZdݑDy0@E$8Wi.ls3MLZ h@tgNuǦ/2^fx%&r(蚧9("D "r4 ^֞i$;vrfe=2>~w}GVyU]MO p$ R{&$(3~1M4}AfGD[j W,Uk0W=F?2;oYgH;6kdT 2%F_FM?&D\##ؒc6JoQ3ZG_xyOcԭ#2FiyAFsB 'x?pqKRȴV5"jZc4M41ɓ:o? DDfͲsCh?YE]q2R`)H1v`4`;S ={xpB_Cx"`>tyBUU;K4ImؤCEz?A0|̪?˲ecR7uzTfQ3s7r !nĂ/.2R/4uJ2^OTΝNS;7xWIac̾s1Ƙ}zch2kTHDFzd>\1rP {~6]Q0^:`'7D@3u"~-1IU=n6Щgy^MͭVWp mZUi$z.͆ꪼkP;qߏ@|Vx~?_ рu{u,!l37fnnuF5![8+ 5BR)J08ƾ(1\, _XcC9Sa>V y߻jl!Ѥ2 "]xum^{O?<36`d3Lj^1'08q"R?BZ Hԯ PPEFDcj ߈^Wm\es,S'DBdYeD65c䛊h}քB}̳7b6"Bky B ذ5y=tp^Dh>qʴ'_N"m'm1V`z;@*v5i;7/JIL?0䱕U>i'6/3z#`hۆ[qZDB/|a&jF^!6]E" YET! ҳg޵2_0u= e%g6p6xoqD G6Oy5?2igӈ1TxiȇBZ}'x≍wT9oߺ0,̭cU^z˫:[# &lF&$2Ny8?Fe%<"A*" 60>Չӈ '6.6KK4̜KI!%3{CQN#ro;SB"v&ԉTD,j `x^<wޤius}c:}T؈m/`F:9(1rlMȶڛ5+sf ۆӆr=敦( 3&v%xUϐ059*DT#S#4̳&nn,i@̰d º=Uiu-1Yo̤ĜEAhxu5ڋm!ZG ̂PJ\  <ϐJCU"(9I bј_e` jHjȀU韕@XVD*mNPK0-0UYC^0"ΥX%Tǯ5j(rc[?@L ;'mp̴can1MO*ItI(h~1#VM[21fAV!D|/=vMG5ĸhQϓ_umvV#@Hhb 瑴 uc$o?v^u{UFOۿM{Xi_26=6n=u^pjH2Y#tEȢz~[J[޺bu4lBQ~=}ajZ7`M$R.ܴ؇iYicN-KJ/{#6IZV?-{HhRH YqdsujblcҬ7~*T͓%/mqkXl"zFy]Od+:xN{mK[hS]ۿgmc{O-]b=0=a,̊<Ρ9T3gtJ"pވCm/x z%a!`T 5 ~fYd@2e>/~D`XF}͂ksLp.uٳZc׷>S"Kb/鳛r '}1$-f̒>IF~C   @ @ @ @ @ @@ @ l.6 IENDB`viridian-1.2/AmpacheTools/images/ViridianApp.svg0000644000175000017500000042525311511672746021561 0ustar charliecharlie viridian-1.2/AmpacheTools/images/next.png0000644000175000017500000007002511511672746020307 0ustar charliecharliePNG  IHDRx pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_Fe@IDATx{\E[f \,aUWA @H  xoq~uUPAE@ UAA.B@ d3}~t:u.}tO~:Ԝ|SB)B!B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!B!4B Bh!B@!B!B!B!4B B !Bh!B@!B!B!B!4B B !Bh!B!B!B!4B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!4B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!B BHeT}^45-i 5 |-'4+歭ؗ S_;l6H)x\F8߲e%\L! 0mREMM>)v衇~Cowܧ{Ϯݻf{ !Q :FAJuݿ=>::ȳo_nRj @gۂ fvaG l6wk[ϤA!*4gm?W)GFGGp7?yb T22pxN;Mvdggb^Iq7gQI+ aR?4<<|ߺu~~wI!Ple4ltY+V{_4cƌ;~R>Qjicl(3`>_x㍛瞟r- f` 4#w3=gm7k~gWB#!D="ſE'SAp^S PHI)_u `pӳMce4dc9f:j̙3;_ ?y]xGA}ZO'd⚀t~a+B.۰iӦ+!D>ŏ`ul*O^@30 , i~gG>? kZBƗ__0n#@-hU@oq_' .`݃" wdվ}M]~ F[w{'/ 42V4ze]{GyWM(בRu}n̯mĠA SbBM+af@J)lr/[n pp|h%`.}]w_2|=z  {l*F!=$AW*[r{ꩧB61)4b̹;lܹss1)A 86Q&qfÄ_ k F@n޼K.߷l2/z;4`tнx:v*vC J8lQ}Q_x%DH#T\,/4:^п}W7ķ 8hQ胿+~_=ڀN Ph3wyK7}qJm¨~=DŽtǢ-au53A\u֭k~`)C4Sx-> a̅oυ+ދm72uPgNIH:/g^x}|0}B_& xTο~`u\`]g̘ѿjժ/ 7/zyK)W-#U i̘V3n!.B_=#8CAkuƍK/0kĿ@N;4s~8X:r>m}R)8bFOiھ_kt@;́Ƿ(P(5B3q)k]/j'm8L5>3{…?~G \[o-R?l@T =x='+ ـj֭?׿CI=4Swe~sq^.uw]}[}ٛoi, li|6VGmc7fBdߟ_CPBq|俏-[7I?4Ss=}}}g<lʯp?diCk# J7"}[\@H#ǝ-": -3zS'@8 /6ǙM6}^vQ&c T0sϘ1x[ܳ!Y\ͫ|sec#)Ǥ_7zV}Gk10)Dœ1ag0UoxD+Sg`֬YgǮmFJ!GӾ+ &xWH_ +d[`k7n|bA4;L9s'-^|kWg^3WQ Y{@1?"cQ%?,By[AK6뎆y嗟 `+e+^q 7e˗}s|YwF$G޼=wpY2$zF F1@d.x*]YF3[ 쥗^olC!d0 6ޢxvoKs J}m;l!X NQ)DyB@}m[Ot 'o cGο{D@:eݓuivj}A܏oˎ쵼|{O5CJԮxB[qL6"ss͏` [_8>4Y*v{Dh=s jF{bu/P>ۦ#8S '}?xTN[Q )"d@)귾aQo1@GGǁ=n- H^!4S sGu{{{{痥ٍfEmtJu#i߷.@3^s`-I;iLU J̹WS(M1zק #2P遺Q}[{Y2 @0b꿫w+]%B >oԝom ,ΩB=~!uU]*5Q Xy+]"}.mMB@ Y6m9AM?C?sd! 0N0(n?dRv¨xPGOiB2;r)C$-J·(K@a 3z]v<D0Pbz)D=tJRuӼ6/){ʾ Cѱ+iq1[s5!L`@}}LTՠxSJamqR} 2>)fz;y?, ԉgϞSߌ鋤RPDŽv–N9Bտq @@BjχW˳~^;@+}$t>?OK)$-MnjeU:Rn܀M[>[Xƿ;t&? _H 4߶JJ-P@ei "Ŵˎz(w(Ɉ+TU.S0 `4(Z߬;BHWE G#K@xЁ}tݨ@BF9mڴZ^!l.vyRœBgV2BqU(YIPId D aV.t=US_ *n}Izo(~0LwiG>4-^<JtsU*jGŋRW3EQ/rK~B*] Hz,ei![h}q^{O?Å,@Yf4Y1cw^3 .)@q/tt7Q:$(G@Lxgqt*X- 07.,,u=|BD8BD1 _"KHj@5;F9}dl[JkTJI{{1;~?'n/htwt%t 0+j(Y|MI5Di[ع Ź矴SğĎ׼87f_-+SŪ 0/w%IU~ q@4ma`BP?_2]{%s_H^Yշ^Gȗ@q (ڥ0ԘbAʷEM$oU(!)tqo2;?^y_迸iFG $6)d:tϜ9ʋ޵q@9O*:M?;PbNYҕɍ1짤31NZv#ZקJ儽1ПA,>=vu 4hg P=];h=/T)g le8)v5l[}IV'4)'tV@%Yjm:}<ѧCeSY+^ާwzn+-k```njS){iϾ*7 |e)D1Vg`^(!,BԺO_30/JJ(sp+ mslzw"պ0:n&tNR^A#~eD~5}p:;P,䩴$-/*yrp;ۦMh Ξ}+ek[Ub@oO RPID9DXy+w!_뻕̝51C6mf @âb 勵1={XBm% J{iJN[+i@Hɪ7g??n/S4 =Hsa~nh1! #tg;oT۾D^.f'1O3Oj` Q}Lk~ـeU%A_@GGvd hRb wRʬMl㘀ΈBF ־Jk}$NJk wT&gdS4 3ሙʘMŊxQy~*W0REDbd 0'h1Q{XVH8i.hț7;W 0 5E,oaDIUKu#m0nVah/ %0 -4dU ePX'ҡpP pi  *h Q=k_H"kWQd'@-49\j/&/>嘢,xMa@;b gw)47RgIy_՗u(f tŸhpP8Oź%褾,@H]$MZ"ϥVf"D:V gB? @#vd/P}_= t,o]gXiESaht Z_^e[,TʲJ9i"Qr,o v؟S^,D?-{[uA)BJbnsjLj؁ :ST_2Vh?P|ue*- $F >*[CIƖhTSz^OHOԣ2rXٺn߀) ME#@'dj.* PBh[<JS~?P5MşxP-a%|?*? @%B/I3sQ徝 97@J}cNyI-~XYwU&),Dnqq }KLcQɞid~p_Y QiP@ Zp\|Uڶvs]"C+.pt>ej=a5r )|, a;&j` ,f / @]_@iEt@LOnK%ZO\B:y`3Zhv:Qy֣30<:ɲP+J3gb˶-RE4`Ďvӑd7!Dvms76~VEl&4M0 RY"N@?/4:D ̘1ӦMCggg,ٙ*gSV+~f--AqA㠫8/dp{d.&.m'۾O\O?4PIg:;;ӃiӦh6a||@~ѬȪ߫c*h_'30W:Bž>tvvBJ?qLNN*Egjq2f /µ?!^wb$t+ ͢BWWW|s099 !J%UKF+hF q/elq~ {zzpB aݓj)&*4uC9𥥻===8Sp/+ Ll~ ;qdævAբ㇊юQ_s<}y(Zon'x2*<~!bdZتP^DɯVd2Xv2\+i|ZT'C ; KhV>qo_o'J)tttNY| Qj2F=BDɔ:::.,̙S+%y+H*)a$8noF=[.c钥a]y>þXTtwkc,-x%4{ߘѿ0F`+dT@"&{' ߻rlٺhUV{S2^-Ē^q}ySw) ~T4H/d}췦(魉.Dh%ʫ!n QH1Mxm!`hxrrUbۯskxҢkRGV5R3 ≐ٛE?fΜ8KN:]]^eT ̜9GH?n^є"/=!kL<_K8J ===PJAJF~&&&p͏dD3ޖ3exQ뗷253, `k244u]>,@c;U`6q\wRN?jHyGɑ 'bG?Q 7ML:OD4waumקlBt{~_Pl{x.󎛋ёO>CT&v'm\hkܗPs- zW 1W뷏|#mwF㦐E@$;}rGck3 :/~6V^Z }B,h*FT˘"69h  gрWIuN>d c3OsWF ye ]G5PWu;k--t! 0hÏ< A! o A/hŘ;}6[mLI6iUC"4jŀ"r. Ȼr/Oշ"&VXtsw+/ں]>EU- _gi eEફv'y^̘1={6.Y9o3!**5TK ~=Ċeg`f̲fۃ[6&$]jEz$*駝K{)5V5GIt(pƙgo_m  ۺjTm<1…4%m+͡u*WG&;g[6m/Sek+݁akwT1cN;e)/l]*4T_Fe *҅hl }kF=ÃV,_o;غu&Dm\.RtM])wLLL\ 7k]\j4Um$[0'w5 g}a_t|`td(Mk$1W#lw)%wbtt?GlI=P-f?]-ۢji)%<@t9.suk{ߏ)Q= M7mmV%+ @rz&D8bddW9m&]ƬϠ/܀A=Pa& 4jۻ@=5lq'FGH'_؎&Nm>nĎ=Xw}LH.Y4z=;v/hG~nFed^{mx?2вk1T9/m4]DG?m۶_i hP(4ݾ,Kё<[{`ښ,M):ayQ 1<2{G[ޮ e-+xG1@Ьԃҍ'.~we2G~NZto'-Z 00*lJoEߨe{!Ӗ;\lvfG0>1vL9.l}* W $PlGzFz7kAL#p>A hgͽۣYQ^&;'R6Յ3`ppwS葬Jp @,Lo !AhQCCh3%O(=dfNJ[e;0::uOٝ*:YQJe!剿xHn:,3]-QNB};>GpXA&wh0mڴbwgX9oaMO3$ڎUJB4k&@0% (tKm߸/ MQƢ-lXb%uS릮5y{W߸wi궯"@olHNFzJz]wŪVͻ8$3m {sWg~=!4BUbinwo{9K@H@B*W"ZW3vl9sS瞏x}S훱݃*5zg^mLLL4}ǒb@x- :BfIe{cY0c f ۾xލӗ./VsdB@HBdׇQg8kBTa mOH@Bx);=Q"jӀ]rA)i}a3\LNN6΀&3fF)%>>axElm8Je@BRFu3d c4Z'PWTW>lB HHybd ٳqƲ3p$gJz}=v:;މmO !"=[<7`v™g޺&3f@ROavTۍBz&`vÚs`7FP*R>{s>  $];<_JBL{& RAaUH @PMߠ[(6Q}:3[C۾5V&@3gC3V#mPۣb OZVIM@HLIcyAooo> hyπ`)9  N& Q/%=00>gSz}WWWt?abͶ'$ B!*w C(nS ?_ _T0l{Bh ȫ}ccW%m+5[Uj :Ff@J~Oo)ΙJ'wԙƨr-|@ :V78Xlݺ QRg`lt}z1 | _T=ax40#Q)%?p +Au2`A7CWj$1fJZ|O~cT'`;QJO< wBH}ȬV?#M?աC+5.= C !~&@OIQ 0<<︍BTǶ7 R +\1'+۞\HHBd-NoN: ppqZmUqլHh!];3V Ը{լHhH%[F1ٳgcʳ߾QAI}=LRǬf?V+mBZQnUVa(}BL>5kgFB@iL47ԧNl)FɴgBAŒo{۰jjBH}M9ssAu4ZaUV*5FBj&```?V>ӧOF)JL&ñlY K'4Q;/=z9<ʶm-ve  N8D8B#4 M3\ÃÃcn;s17o*7$d*J4coCClsπm??gHH06;z GEںu+.b I|Iy<3زe _۶m?7f43"d Y| roݺ˗//h $MJ~3!mdd',ZO=\oFH3)BK)._O~6Lm?22ŋG}>F3DK/ť^ZYd||g\uWAi !4D뮻_H'Ɋ.g>i|;*} BH뭷ի))җk6ŐRBHm/wߍӗ/+]x   T,Xv-t()/uq 7೟ll'?~=w)@)m݆իVU, Gh!5 0o<_Jx-]I=iyX f.}( _ yR5;ڵk1NJG7n܈ysS !$-re(B͛q'=Ǭ  +Yor E|S.l{B@|˸k))SO}8۞B^ik_ſ묕+qw !$*V|34r9\ pnd6!wQU9R=^_|E{cB@HzU;e)4uUWK%?!4#@exO-sr!ş'wuN_#  $egƈ7rI9B !͛1oG*B@Hz\ߛrYl9^=!4#B4u]|ӟz+۞BVkr9| y2!4/4>^_zɥK)\c ߀/^x!şB Vkz8(\c_J{﹇S.oܸ Vk@ۯ]'pT&tȫ43ϰ\m{q֭[#  $=*G?bddKN>=Pq=!4VkTmO !鈐'@]w+5@s>OnfB@H뭷U)@)׾U  $=RJs 2^W^qP  $D?Js |?!4+O_\z*f] $]߰a͝Js)?T&믿`Æ  rʅB!;nIy,[lB@H(s҉'pƍq'/HeBhIQsZ뮂S͛7cC ! ׬ax[!u]|߾kb{G*/=ґBh7z/U;>RRHnmO !w܁իVQRk/G*B@HU;e)4u5K%?!4#@]cފO)?!4+< _,RJu]8ԥ<܇B7by4׀pʒ%r!th8Dz\m.^/dօBR TiE|P1^/">hL !鉿^i?W|"oܸ /9B !)F⬕+Yi?44EG*B@Hp/?rۏO.ʄz˿=M8 }l{BhIG<ꪫXi̧??|Minb&d⯔*V;gj P__y%۞Bw7N_$@!^+K\w# ic4D\^̛}<N\t!)%x p ذa(u]2)Xrl~}3şBE_?8ֻ@6-k3W^y?!I>(~ ' PJCJm۶_^K]qt&2c iI:;;pWwwMIQ_֯gYEEe(=jHhÀ,ĝw܁; Pʸ~tufZ4!-s&''199 q ;.&''(LP[[7Ȇho;-H]&Ʊ08876oƌ~Y8LIv]OL`ll ##CH:mXZ4@7v8wttP b\,!-V3XUA{i<6>`P)5+ ! - p@h!EI8S@uxB B gO!Սit!$dd\QiZB&pIЖ>/gB% BhhxBjxJФWܠ6!T5;ޚi.d엣u &xR._ )R75_)FDyBM+UÖ4Ĝ*#uƫ!~A?o4dO!7 *o(4 4U(~+1!M4]xk^?W֤ X㦂zלh%z5yB8lh4 ,@N*Z7_ռ !-- lR>+/ @hReg#PYB?z& N4I)7EW#A1ԋ& *ȶ/#hp Ry!ľUBAA@#0VVtEg j @m`}KYBԿ & .4x2ZBj^!]U3 4_ItdUQ&YBHFM)RjKLRi! 0Y *9" L!S=J=` @C Qԣ!>,-!]1Q\`LJyEYLM!djJLlq]q44rK])tl鏑 )j{a]2@4 sW:`D*G%YT? H/q0@YI9^voZ@BȔUQ/JBT3@_8Pb@ƪ @ @ir_{_J{Ú4MF RK+1@i844Mg…Y0# 5!-'xT-44xBRl 4W_ u r{7M@gBUxw{`E&FT"aUAR֕CZg_)ݯalzHAnvk(NPB8@AY7UOK)97we7Vh,gPJmt]lBbS &^/#M{UthJD m+\I)w]~X3%P̀ah i#NG5p.R)b!LDM }o* @+{39FZ |UxTT3j]Zi ICc=C%TLAHsn*zO"8/jnBP'2t Йssg8{+3yO7n}t 4lE -?RG:0Vn`97wSVd3 6ge!M Q?t"-"mGfnEG6u9(OG_@il>N$;g &7ɺf ƠlQ !I zTվM&oPJ=]_62(% 0(9*cUF qя< L638RGs*7d !f} ^*Ri 2.!W.PJmȹ[RCįC 0f,ف2V R; VkE) Z>[u0`&C6#}*E;>2QJuA>%׋Gx%*ZI3!P苀apqb2cnDS{N.eld2GnB%)P+v]7RD}=XLD<&VZ2 جFŰ(Daq}.ߌB+<3+?K%_fBlKmOQ]ZĺiifqOPc nڄ"?j)&WcՉ)8\˘p 31!}coC^nR/M&d8w1'r)n TOQJ }PJh+JG+QW(^hf{0LC j iS *xm.w,F1Bh7xetCR3py+Z'jT! u뺏˜ޘ'.f4o ȐmV3"È>g,Bo~& }ߡk@*d.tg9@1CZz?2Fu*6""Hߌ&M3(Fvy@^8H﷉ĝ@e D[څmt [I)_Rnrg׌;T3 @t)ˮt+^ӄPڛD#RW}HEP S/l9z}K儽s&RWv2G8Ā'_Bt?\a-sҁ?٪tC! -@@Pw`Ez"Cs3JsgޢIׅe̮; Τ BR#Rɗ/)i"H䳓qՄ9Y׌?305c4_ n(.,.׶}}t^i[#uQJ{.:lqf!ށF!RMtڂ|:\Mnj'xx)9ooK*h4s%55(>oa̸}N {evg{G8BB. iAOA6)&mע~O5S0;9-8]meIاdel ke㚆5f,v=G-VOKetQ_-<{,kܰT@v-- u 2$wQеd,%ڗZc3,00)1؊6}&:ҿ0RP pRdϤ vϜ<}`= @ELc0?hcm9"q D rϭ uS@ǣ^5* {\|N^5RZDD܋sO`BL¿xe뻕EЁQY=ڷ%$*u$" @ 2@h5  U!MeEl kZ k~ȜB2:<|KAUmB," J3f]Tx'GNU~q  @#"jra?FZiI>OfLU@o>}~ ##:Y;eb>%tׅ$c70g:`?gf率N&L F@ y-ˀ SA&! c :EP1sUU0Hm_D2DE@yZ|^Q.}1-12[*ߜ030 b=lm7?S- .s+B>^=ksn_)aN03S 3oV3p<%ASA voi^}*JD}*8G"jVGEa"A6h5"m3zi@^ͅ}q>ۤ"j_X̃m\ P͘C 2"`3ϬE2d \%,;EZ.,نLH$oKDXL)Ņ5a3GՉ2a&!*ڏIP+8GH4$(D/ z-d2@MrC+䭙W~9E,$f Y\]اC$"\uFeqmTsͶ$0F5,7͜ 1,O%t,o& :=7hcX4%X>NDڭ&Nu zaQ_m4҈ qHQi\bmM`TB, /Y7 d\K*ݵ|vh+QWل߭T \Ϝh,A 'Ul>7Q,"!jf6&Ꙑf0gDޖZD8L؞ a [t9wߨ5N *PW_$¢(6Ĉ*DdlfE⾠_Y>O}_f `a)$L 0D %ČDsb_,B&@ 6s!0D9)g,p6 |&')~ DڌBau j8X5-(igѤ3QsA?7(oYԁm۶N!z[ |%a>YUFdl 8VL)J޼r&fFFJ3^;H<bl ]H>̐f,D0;FVE}p; :FcD0ɏlˋ HXܐ ,BU',5 -"A[Lk]-<Уj]/ QD@z .k6CᄈP&Fiٰ`O凙 '8-w>goѠ?L`3!QsCjAgH?-"?Da-]x'H ̶##DKc ;!ϵdl%=Ǭ"9l$n.tToQ|-'hhf'=3?;(͍Qc~2^Dr-!$٘QfZL4U2V3!*&A]7ĈQzP">D܈UasYuPH$T`~@p?NXUG1~JJZ.JzJq?s%5(8*#*Fv$ #FU^HlBU"mVH]G Sܪ|)_j9n#RxW:}sN JiITvg}O4 qV0$1'.cS jKQLOz ue*A?/Nf#iczfyUCp95՞u,mTF#IuVVM%Dc@X:}QYK=mU}mQj F)„`]M3G̩dU^*mD#4Ihƨ?ARbqj!4QW3lpwpy|Q !BZZH!f!B@!B!B!4B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!B!4B BhB !Bh!B@!B!B!B!4B B !Bh!B@!B!B!B!4B B !B@!B!B!B!4B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!?PhbIENDB`viridian-1.2/AmpacheTools/images/ViridianSimple.png0000644000175000017500000000330311511672746022243 0ustar charliecharliePNG  IHDRIC(tEXtSoftwareAdobe ImageReadyqe<eIDATx[[LUφ.PYZ5)5i\ &ԄE-4}4`5D}DTckRKMAZhln6"*!Rvw39 fg̙@@Ŷ5C؂bsfP1J J|%w3tK&R2>^BƇKӇPC ;UBH Fs5c*mtTEV͛F}23,|d"hiNZ +*رLL ̘O"Lo^YYGCD%aE"I I-?>q!ų}>[zs_[2d%'@8I|}idB!Hs}zPQW'gi% ) Q+z?^CWWQ!u% XIajѻO{JZ;_y9pK$3wr}H3ܰW QlW]< B7<}?`4axxϜ!ph_g{N`N\)-cd|Cڻ[$L(R5dz!U ڽgO$ oGBa]jBɧ8ɞ4J =H۶=>HJ I)[V!*a:Qfa|q+ߚ:y$?PX{H}BÒaffT^M}k %)e>q"TAFsÎ%091a'B͠ &T\[6EM!o`qdW_-4 IÁCPUj귺@FpYF$"X'X]]o9*쟰FKɑ19i`Ꞔ]vK*oq8i92n\Ė-AMOJ+ }Ӻ~_BۡvΎ(eJ1:z"}ʊBM-PXN3y FmY5ͩ#PCYyEBŏ2%]gzZ|/E_\/.*Ƙc & c`8PZZzxr\/mm ҂PL I޾^ش^pݺVyy7ܚ6R%r0AKupT}5$.ėjJ&6k)@nuǝ _tt$L$9E5UVV$&{p;sˇ:_:vCMj%)Wf0Av0j* '؋xF5u &oNH|Q3%xreoG&i o.9vnYZ2pmm}O-# Q`rj,f>Ceo>I&do\$I at$i$F;U,J̖爵 3Ds%#1 հqpOfIENDB`viridian-1.2/AmpacheTools/images/prev.png0000644000175000017500000007052611511672746020313 0ustar charliecharliePNG  IHDRx pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FfIDATxy\G}s{MbI,Y2V'$\&7! ɂ&ۀ a5.8 /`cF0&b%KhOS]gt?#Lo3]}߯~%R BHw !B!B!B!4B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!4B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!B!4B Bh!B@!B!B!B!4B B !Bh!B@!B!B!B!4B B !Bh!B!t96A! Q]'O@va^DOi/W]m žR?{Ǹ]W_}x.s B:'N8y7>\Kj/#t0mRC-NhS]߾}m_bM==={zzrMB~ ;!(H)yΞ=ș3g~rG7+Eke44ɢ36[+o-дj^fffw{=w{b b22xϿu{֪Ug bgM]wI&!J%hq-yo>|{ y!Ple4lt9Wر/ϯZߗR:Iicn$3`ZO~vvǏ=wqe30odPh HWW^t钥S)B@A?p!J(D]}'9 MZx=)<{מ<_{k\h#@-&G^ysccc/sg$*v_G"f?EtHkQR bGm Ph''"[ ihh$Ν;G_җeKuA!D)ŏhulo^@31 . igG6?G # }5RXJ=O믿 aT+S }W]uK֮]{uPXEQﭚUfF=!~QU\n>.j!nNcffx}0S6@3 ^/E/zѦg=Yo^H)+:>o7{[:1vvPB7TPQ qA@R'N|c؛|cZ6kf6 @'s_@߁ڵk_ﺥt5 {N%Q"wb*BH')z%QW[(x衇.*gMY1S 9߻/yK=00[bdi#*9+燮^Bh0 KbA}AAz P-Ug#PݐЂqGgg~W|K_z k:%@-Xwsy8=G҅I){ !RF>T3'|8)C4+Z|_ |.\)z? _'p9B"y;;%!oBY׷zۧ0:2v7z?|Kh7Tf^ ~J h[5c thm!o*{#7U*;̀c !:w.ʗm&c @s{:}"SB}Tȏ'Qm?(15DH˲qy.s>_H_ *d[`ks|?/P,2v5@ز?z>S(lAœ_ef٣3Q k֏El F9\_wo 9.ئ;:u%Q*\@C4hy<>/ӗkhh袐C֝Q?o cG ^xheV 2P?MU>,#v ĭj ?՟F!d0 6/[nB[r,\L.*"<DzHvXJN'$$lC[>i|p8Wf{6*4)._{lX<ʕ+W T ʓN51.W MR (-~<Z^Iv'G^=10u_JPP%Q{i3^=T J?RD cYV^JRa }*w?>1Pj@z^P C՝-4xiW .P8++F,#v'5vָOONK,5Ӡi}b?! =YA~!GD0_W ɩ}߿w @3BxZ^!b.vJ'mㅾϬeC>DQ~ѓ¬]zU6ܶ#"PP1`kyQ:#YOMNMJyb*TGŋH_g@ /qK~B] hB_ Y@ .*"v1Bl1c`-[.8twY"4hs(;<<|y7@3JkJ Ey_Bc*v De_6qYsJ'}XY%zϦNR_]h{x+Kba`6L.Zr A#z̀L_$8 X7 P^}>waᐠ"M @;/]rEJ`(ٌl;(Y.u=BrᴿD8BD1^&KHӢZv؍rf ٶ-URG+%ѻ>i?W3-yX}g>P- @&>G* Aqk(<䤍ba[/ ŹtSğŎ׽87e_(SAP]IRUCQv |s<Σb2hM wll7k8 9Op:M??P, /ANYڕٍ1짤3)@nV?FFO@-p_{6k> 4-\B-, :}mY[?/%04hEqv (ʕFAJm1oVg%zSCfYzu4Kjg(a^땔P`$$= Qj]( 2UWt} **`k`r}!X?$:4:Ј,桴Y_TyYy F/+^ t* ӌluO*_˗/_wȑn6E)UZ)u\ W *6@ m[`sՙMYDV2wF _Ty鲥{9#EA _ Bz-OFYFT4j%qL4 $@^d ɀ-Q_*)0 (+2~?bY k2 5ebP& *kMfO6׏!:/ .Fq6 @a4-!808pRURh#чr?xaԢ@뀣STOhf)*5Dי?:.TAhtM#.J.IC&D/_|Ñ#G`?chp:31Q ˊnB1W&GY: -޹bB:О` T$f l3wcDSi?dE>_]/qWJ6DeGS1_ID Ucĝ?鄌@}׺H - ]P˭Ax hZf\1eL& bE߼M?++iҨTU$*? 3>Zky5hҞo(@- 4-2%Ԡ#wC DfTe=0v" ~M]Ǭi?nLULB%;zZDf0`q΀RRA1DcA e;RSIF,vjvhMP `aߡ h gPA9k( _W`7PTCՙÌ]yi ҍum&bn>T?b:T~g3e ihZcB)-a^N5tP @ULDN&HR3~SIߍZi~_3޿eOJ:|?@%"+Uu `Ū<2xN0R }7jE~e .B?eR5PaO(,*rVb715C@ڢ#)&R ڷ,n/i/i~i25ڻ/g p F@A-]6@ F uTl2wԐ\DXw]: @U9BИ,on{\rM*d[lx T)(."}7NSfT\XP7~֧?~6`?% N'fU;mjS4 Ҭ P5>/vN$Csz@GZ{k=_Df+2TL`09(dQuT٪OSdNTdT9~Rq.|"^OGMs@ @#ENYH/͚xO}XP·sh{@@OduE;eRP[VmM1Rʕ}SEhZ)q)[ l"P@'ݜ H[݇a.O5@hZ޽Ԃ#ҭ>2\v`B쨦4fW⿑e9rh4-T_vpiŢz B#*[CIƖhTSz@=KC4Ӎ@;\0L7_4uGLdTh1)e\YyQҩ& cem:@Phڧ@i/J{5TʪB(FcJ3nSӤ,*A"K^f?&Zk 񗖓@TB)4_σm\f'hdǘ7u4jǥi&hMCWe}"ބ(ٍcم#07BuXkk7Xa=/?mhp'O 7My+##.}c·M\ )d 4mܑ+N nw#.,MT`MXz5  rr%SR(x!Yx|4ޞ^l޸߽{"MiQZ'>Ϭf j4=q5{9@Ж.D*E F5F  EOOr\ׅ8/)()#*\۷nCP5\}}SǞ}eo U*y#QlD[/ 188 gvvBB[Z98wXr%N9 $EmE=y 0}W0>2}Oh@>RY"M@T>/;$!4뺱y̠G߻ukaGކ-|_q޹i&yD)e0Uݎ3Y;]Լ=uW&3r144~0773g ?;wsV8[δ0ے ZaӆMرcӃcO^)%kw᷏ U9^O4mշSӾ*K[yoz{{SurU1>>%KgM 'Z:);wbhh(X_خjD}1=#fM*;h=`Ȭ8< zzz.p]q3Xrwc g ͺ_}=gn "i֪) q8\ 5@J|>G2 V._;vbʕh[̙3m]e]50111`ll,d#g˴a+@Ц^Fgۂv}(pPvll)v؉5k`ddzzzpĉ8Kr+Zq hQ=.LnJ-bm 044T!@tT}0m]%_)`CCع}6nѐZȼ兗H8BKڲ0l,QSS#,|.[bǎ"dھ[YRGVp ?pަB9] \ׅRJ6h[ c/BhqzSŠAsmD05Mշ}!_Z&Ifk,tĒ`wh+5ԁ d?N`ú ҥKCs3_s\dʶsn[6mDbK]+Wm{>W%.$4;74׭Y}_+Dm]]uyy\̶1IXXnh" @`NK󢳯:_r5&DZj*k\.”Guql\w.&'Ŗ}}}Y~juIFb)`K譣+z*LNLb͚5A9_ \cPʺl8g=&&&|P?_7#)Z~&4-^mGe01>Thtt4(6)l\֯YHe}㥯HʺھUQS.iZ%:m2ݾЖ~Ήp~t65+`|xőfj.F-_fi h7w۵h crDpmM?3m+He}EOֳ*3g]}M0c[n<ٯlme/[ذaC^Z۾3]Q+9+"4sIJ"/UK.,{{0}۶m-6 ^f&N(R36<#Ŗi׹*Ai1݂okɲA?zz0m'vyLT>G?09>y^vGwO3hIwh^}69~@>a||Dݲ:5q0@"F-um \uiHn3Zzkb ?44Բ)BXc͒"&s/`73Ǯ!McԮP9_3*|]/b>n=GטUً`\>a%3t9B`155+W,@S+uY.v^}[ 3:-PkVz9{׮}|Aim(/O-091KF|S.Z+Y,3sg["u\ur !p-Uκ،z B@LW-_=`Æ sϭ4=mކMؽk7V^OkV}B Nd5). Ԉ)ŰHm.[΋rJs)ȢriŞ{}̴TZQ"q | Z366<}{̓ȿ:׮Z=v[\FGGu!4lB#@CCسk.Vkrۯ\{wƍWS @OeP?>`!UYJei~ݵ[i83`ْػ{op"nEZ5BH&Yޞ^Js-۽[n4^~օ? -~R05>Tۡ\' ދm۶Y鑿/4^@B|5%J?0}SSA9XRiS.S S.QYr)4;7,f!߶wV#4!= QƋS.;[bjj*8'vm15>БJ-;RB:S6o:Ҝ/+e)U>R9*ʄ&j6Z[CyV}7l{#+-mqlߺ vaٲe*HZ+]܄3N׭]{bgVk`E\޵;trܔ T&LՊUػ{/=ȴ?ſ7c.^d??o#  dk^inٷ+-Va݆#>'.'TwdIZ=;TBz h/]7oJssKbqڵ+ȴ- ;=.mfi4׊w; \t)Bh\YsJsMn{!߲vªUixqʅB2Wb}4ׂߴ<ٽk֬̓ӂB2%ؿ*͵GoX{źubOU !4d*@##p?+͵׬^{ENHeBhXJ  b^Vk/_N+1'Ŀ\inB9o\?CCC֭~X r[7.zͲ =س+\iN _{zzXi.Oa۳b4^]?墺hP-G3"|>ɉTt'{~۷?8R9*Ϭ !4d*bp9³4ppx5(;K/_^Q叕BvcbbG*3B ! Pb&}.$i\RJswڵk3]ۻؽk7.]i?۞B26`Ϟ=X~}䱾4}; nÔ6t2şB2ի`޽8sYim/`Xzud1BH aFWa-nRE5G׋P˱~l޼*@ϯ q&ݻ7rEql?dDze˰Or1+pB !a}V> (bg`֭Ϻ0'}155U}\c¾}m۶c}}"=Y\x`jjpSo>ܹ!7+  .p۷oGP@ B\Y8r!a,(TGE__000+5YyӃ>7o|?p!&oΟS./<,eXD5X2\.3Wy=U+K? |_˗/իCn5^KR+wy'/CJ )%R?8/˘,P,y^ L$/C>77jw=!44H){}8}4Ξ=Y4={Z?ӧ133YEBhX$ۊ"76N<ӧO&O0'NUW^G}N™3gF@ ! % ^sU!a4SOK//~ `B@HM8uj9[}^. 'v |E _|xtm}mғxގQ<ϵ >M@mKHG*Sҷ|#'>fƷ}Xkߺ[Xɶ'/|1(l 5pW veH!44M7wuWHBA4ki\y7t`nh a&}7ߌ!䢋*N5p^AP C@ICBȟ~;މ/}KAZK7YBZ*Db7Fv=m|OOOK.?SNR#!44W^Sq-6=/?XB/DgΜW$&RmO !7~ګ ymcM7 ~&'B@HSx bǣlz>яŚB ! }7Bq^9mo " X /|w`dt?kדƴ;N ᷞ[,L ! [> Pz Qm_,7W. ~n۞ N{>ǔt~nnW8J!Roy+뮠Z M@sڞ  BT,׾-4YBZ*D3335W*5`FBhi eli?|0.2Vj$֘cǎK.ÇqI>})f> +5B@HI?9\~~_Z 4|H !-Fͣl QL+5B@HKߏ^Z?~<ts;?P}@h!MoeJymG>[?~k`fggC&N&B wb``B rpJ)m@|M \_kg &4 'oI ض+5 }."4曀}Xd /袊k(Dk{R xƅڞn!-"^߀/~AsM~￟E !B$ě&|_ _(&qm?==+. !5B477k&8ʖǎë/ !5Bt\vɥx6xW'`FB@i Ɓ˯?̣le:W`FB@iM4O./~ eD*5^s58~8N:Jh|e\ʣl[`77ӬHh!M6z!\{5AJZ7Fk}cŹ4`!:x {uA4j;BԘ>PۛBh! }7TFo}C>HX 6"d_d/^*Qs|;100 Y2B*DG1<< @P5JCx: CB)o aJm?77]Z|;a@B@iI)T hN۟9s. !5Be{ ! h\OOOK/cFB@iᚫ3FO>$.Tj 4EW^q 8֬H!j ÇqůƓO>="4cǎU7<ʶ^СCJ@h! Fx  yŸ5y;0<4O@h! 1:6g4Ķ/0<< }ƶ'!!*Dz7/55\Иk w_P-p!򏲽뮻B}! h\۟:u W^q?zG<<BZ^(cp٥<7B/DQ?ad4J*5^Kb+5B@i ]yUPv/#<*8<BjlӟEILJR#3DE*e{٥Fe˪u1_@Ru{}P2ߕ'4Dlicկ]|~m_Z"4 y/D1>~ OdBHÅ?V"ƶ- Ѱ0!],D~/y'FGGW(Є{ރQ_^!ygFDQ$[o~/y̲R)Mo@?~D.| D"9)IY 馛~.nN=_??9avv333w{$#S,ndN<_{i[36sI V:Ļ0bViO>ːwsrps9 ?=._>,E*_8y$/&.w훣UPffg~c8S\ [,֜һK3\Hv&+_e}OA=}Qs*a[H*( şfI)w5NBNk]DyޝNaa4mw8U<oj<+&?>!J>iڶ c@@Pf(eS @[0IJҚ$D@xפR),4m7,|y!j*E ` 4bpS| (eQiUl~Z*Rʯ$Y4 ! ҉ +}},ŘAy7^zM@f!͉C#eIyһOcaEgdN8R(|J<6ҨB#>a^VǰgOHs̖'RPJYcL@u4R*e%yWJ? @#RG24[ S,K?p bu}Gʅn+~UCk$!) S޸EQ?)zۤZ/cn ~h2*D߉7eP(&9OzPPgsn1!6o:u7*!$KA⑪g|qRʑ ÆԿdtc #e1Y]6 RܞF"a}t?IgS i13u/ f5!m}QpZ7 J?qwF}w1rEn TQJm)p_!_f;.59}b㫞mvM;\SJ3uw99'VO9B:I!~P~_O, |Qׅ/47y[V@ ];u).j^Fߵcs^{m0TJbxqBТϼ?!!v[VRR#u8#PU$! åOxR&'9K2u. G*zL@~qSf&@_,'^0/RJusA>!L \VJ+GbZ!EctN~PH4@4,aprm\:N3Tr^SB%X! Y4 ,zq~MH%R\)5/g?A);9[,s3*Bc'=NJ%R'GcV}?m)jD8ʖ'aU/e 'B @8 Hפ5́Ni[񝺶R5RظTDv--u T=CtS_@`"0gf)2lYDe*2 Y/)]O*RDQm32&—F&r<'* *㊖H\u1Wy.FfKfyAJї֦H0 i80+Tm׳6s.޵_ߡgf,DYDyJ)QSQ(vnHi^~*jD}14žFӤ"ZU'Eq"Q_hF5S?.Nms>Ӹۼ"j_X̃m\ P˘t1gV" zKX*OՏ#9-rlRa@cD1cmqmMAIG6Geu @IHlt2j3*E5zF5 ^;i mS*ykUD_4~`_3U@Nj0 0Hdl&LE"cTfRFP9)^/M%DMXa9 Qv#"e"m7or"ޟXD"w@ {X &{W^O3cjF)M84{REMm ũϳd"cs-ƴ2(` dBHVufNVզݔp=**u#"\v,clx/ND$gH| ̎U1B$үDX,L-{4Ү/&C&Ke'M#HHzlֹ8Yޣ%/3˄=F"5LihDϷ [f3N)"Oe%{*?91&DZd lѿ9{FEqDqFF XI~ BVu @r(F0~L-#q3<"ʒE-.!1f< q qeucQFԟhuNz7k ;*͍QOc~\$/sRd9Dوʖ\(]Eq3YoHk`GUxSUl(fcD(=M DڈUqj4$ʿé$$l+I`.M Rj5C=q5WD\eܴs54(8)O#*Ev$ "EW[^H[M*fD6i+. 铄)mU>Sq.؇HYF>ʹ6g}O5$;@U;'R4u fG))FhuDhg=6ֻ*Ψߗ&P1=i<݈Ox ;5t,mRF#Iuվj+|Z?S.8~d!AhY8UNYO>Ѥz5(*5%D֏mg-ލ{iB&ɠ_'jZBDbdeJjnh?gFk5#Y,~tx,a$TC?fN FD۪fD7.fHSfVzM~bqE6ȢB@$cԟu +1P]p8m|}uA aŒA;F/cB/!BHQ ! !Bh!B@!B!B!4B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!4B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!B!4B B B !Bh!B@!B!B!B!4B B !Bh!B@!B!B!B!4B Bh!B@!B!B!B!4B B !Bh!B@!B!B!B!4B B !Bh!B!B!sO}}kmIENDB`viridian-1.2/AmpacheTools/images/star_rating_gray.png0000644000175000017500000000041011511672746022657 0ustar charliecharliePNG  IHDRsO/IDAT( 0D'g!XYvB" (,+yivؗ!IN4 |Ѳ,A?`_u] ֧[a$<ϡ0^I) _9MOQ( NZKc`WUUܶͩ'I<ű}86_,K'8+2H)E^ 8ݶ-RWJqǓwt]wS' Tƒ)1IENDB`viridian-1.2/AmpacheTools/images/ViridianApp.png0000644000175000017500000003004211511672746021532 0ustar charliecharliePNG  IHDRmM˭>tEXtSoftwareAdobe ImageReadyqe</IDATxiwGҾ HHBl{9 `̼zz=t`a6b l ?K)J\""2#3LpSU*EF>cBCEJӧ]Û @a]OkP6]?(J;K!$wbP!CRW Nm7Š,1Ķ!nGZ%ƅ}䢇-w(Pr}$d!5b1QL<ϬF1@P;>P!wm\pl>PX xl' ̃6!/<55bA (B7S.j>v*V=@@Xߡ(@mwU^`#L;P@5V=@55kzkj!rX!qX!q8B⮱B}_OS~rwA.PGu^` ]W7] Dvr`-fn_GMw N6&.t,Xߢslbb‹hl]y0{:,xw,S`~`CC10Sc/A DT/C|E^SiTMw=wz<N27Uoe*`leh54yF/s[(z&*~1 D|VA_aKKKUeA8׿acccaJ"m:{% IJ}{62r3,q)KJZꂺ5618L_[X[ ޼eC֭vmC26{4(twF3` zMCDx@ dB"}٪Xą.8ڐ0X\( s>r[0(QȍaB2"!nɉ(:L͠ !Pr[#ie6ETX_#=AJ8($#G8mZF2($e7h\@&bjzD`˽{cƍlCnF^d$D|TD@,@Hot)DDS$Q6˜FslB@1Kzn}޽?g! XԿD嗁hOuv4L, bC^c!\bGhQ DI6:zG_zsFY7h߿<6Sxpj[<E&*6X"JarjRVĠDCNMS%?% E. @Q(xv5x7h # @Ȅ&j,/# }@]5ExܜbQ6T$jE. / ҬIM=/ K,{dm?LywYCCv5%WŵMbyϥ@( Jh3}_7WZ’c.;>+eAdȆhT,gm#4 'N&BsqFL?wccC?e}}c[ |'R{d m'Emm-vGi[^|cc-PX"N5S@tb(s^򨠂+fP9vBdQCuA% d(n61 ,{ds" z2^*ha"| @ Ic<:pTFzlaN&'r۞BAe=:LhWEb@,/O$ɦvL9K(=WӇB\VH-L@"o Pfkn6F"Q 8:Lh!)l^"mB< *t/>xQEb\6 *(&(O ! C(Ǟ6dSHtѶ\[|B`4Tml @цdYI666崡@rڅ96m6PiOa'>??# q:t dv"J*b? і괣:~c~^ -Uu0l-BA4*ؓ'OP6:xoֽm(D;C'Ӷj}= ?m+քr1PmH<( n45cl 2EkPy -Һwoh? yB$G0 hzzYAA oNd!OرxI4)+++CuڈG@- "]`ii|!iCA`1W̓ lNׯB < z*^Ɠm^IuUxe;mH65F1"ڿa-S5bkEg4i٢G%4 kMR,±H2d---Z#M:|^@:,z^hY=3Qlm8fP,,MeCAԡ}ٶܹ hD;xĭBDq|ݽ %i]y(`цiPVV\T:#>6t(_x$P,Bd;Q^^3VDFš9m)yc]FN ͿQ4͢TnhԙI@9,#^T u446m/ ) ęgb}]ұH'&&XֺtO2=3PeB Ά"{,b/}lcc>|4"*T5dMMMc"6q=}0ȑ8&|h\4lbS,5+TyG{>2NPReϠ^(ME4ؖյ!jOrk3Dn:Y G,RF"#/X__?+++GD\C{8D[h嫿6cm-G*.b6ԻkX ˾2 "XD-_ENMNyӧOYmm))w M_پɳ6rS} nO@Ul(C40gHE@EU%%%4HI97 N>@E-''іha LRN,Y__(#Oi؞NđEZr+gh}\{Ϟ} ;[[[FE|so&"rڲ_>dMMM/.-ܐH .a,)B%`VUƔEݘJQs[T:"m~155ŊKmSTB {j:nΠ=) hP{9ˎy-b6=*Rޮ_\ZJ( a'KY[fpv`\ ~C ֐Qu-n7e/cɺ$IeI~.AteL;,h7q?hx jk`EXH"]TTmel⋘{βaWGk*E4VUUʪjC0vPHZNŜ'%~ R-?[$`ȹ? f\S}X.ή 9v_g9miE$$$ܓ 'Ǎ.ܾ= A2&gTn" Zhdm}MNM'@ G;&''PTzX#pNG9MՒY]%JEqcc?`!d^"Nc|cuM]]Y|mSa&rm ;CaiP3Jbvv&E=ru'(1WH5Y޽91VWWw#;1lc(Qwoe˄"LprU) V j-i>\OګO+J3== n L*|^?ECEx5[449jr@1diiIwtI$/_ӈhLgzz:p ʲ3D7^lXJFD)++=B%ڙО tQBM9.%L*#-}'Ao;?8VJNuuGєD|r䴁XcssC3{ih{qp'MQsUhF C=l9u}qqQ?tHkPpm袙)?`79In%=dI?\ R1ޫ>rT?I{TWװ=-zOuOTFvՐqE' ]z!'jz\fd2QټvCq}&]\RB*v&ƼTzHʹhD*z2YcjKܹB`us[R=O^:X1V B1 ef[XXݶ` Vh^xx@e#"fP,woCX?~`ǏKpH$왐xetx֭X 3iD2#mr۴•CW3795Ɵ :A!ʱ1[3ShVni"29s#z1][f:® %g oXZsvZ'}mCl%Dgw;welLh0 Q6svD,b/׮}˝ eX@dyy,M+$Zl1"bٻ볎RnhKbjj8"Z򽾱 }hSY~ n`al#oD{{00P 6tB]mg}Ui:|UmP,Һw]e- R? D;vМٙafgX=BorkZm vVSSkU==TCp۰˥!@#۶۲ueeOEq3C1G}\yutvdңP&<]C}޾ 5 Z|iܓt"}}spX= L35mmmZ,E WW!zO -:ՆZAm W@s͚"ho`XFNS<*,}7W/7Նh HNY__cY̽6cum}? El_Ol;AJI۽ϫϵ7Ɇh tttZkX2)xD}x+dz/{BG*6>te&lI}2. ՆhHH![ .#pˑׯ Z)[?pN ;q&mP]ms>ʲ~dsz>t 7M0dtvCgW728VH5Ey W B#):m_37;kz߳>![[[ML__?WLuwm_k:OFCFmD$Oشkl$iZY7]0tvvzGcyy 9-r}v^g|g( H\yr&hX1 {UwΥ7 Bf] Gl؋$!T<8+,t>ȹB FN5|]sy/wv@ іll;m;flh{ۋ$[vi_& Yga=vL_D(ƹ{D5@UhGj8+Y"Ͼ mK71XD##i{iIIٚ}zXee J{Q役cjCeAKrO>Յ8ݴ'(?x7F3fX߳ ^T@{a2ḇqT9^GgC!ڒX\\dk54{kĺHg;m P:n.[ԎoE^ ހן ɆhKeVsۻmNܞ_pH\ cp>:nX8x3EWo\Sw#2\nojCe259v7XҲwЎ 7!'Xaa㍡T6Bູ'+{Wm^6D["ssu!+..&w> ЀU/*~c,'4R+Ύ"GՃhdccC4YD)v[ʕ/ǍLii);xdϽEYC`n\\CцhG?F4=d~r7@'Nb['XxܿB262"l~"CC4mE{f6Ol6oh@q{ Ev *\D\#m_S5ce~9G(| ȡÇ{F$@<9Wۦp/m{UVUY&MB~(t nh ul*sed++(p #G"iy8h@;hw4Z x A@.hKymq t6uk-gENXnp%loZTC hC}n="tдyS>g. YF,?khhz 7sp9 F}b h4ɵf==+++2u9H;z(kaWg1Jy›xjT //,v6vv<x<"lG,r,I\?GTCDo -5nnrr;r;$ :Ԡ!CXYDjhs]Ȇ`{#|C ¡}Hh=͡ffٞ\{ffCJFϧy鍣x&=!ު4Ͼ eUmjJJJRE\?o<7wNNq9xTIz,͆h* >s nj}?P8vԘ d]!iY)-!lDBi`("itY޽'(L XsxUZ<8*H@<ʩ-..XvWVٯ+X.WH>"hK Qn5{#4D lD{q+ؒg\=u4+**.@[)цh VX;(@iH?k{z{m%J3UQ LOMk"Qk;r\~$s\+(9q r>W8n/ 2m,,;v(6DSo% NV,?j8c8PE6b;2gq,B hj_HE4Ez{ !A7-G_~U7(!=K}ݹs&sz,z,rv b8#4[h8q;lz nی tiuuۭʢM,l,Civ~f0 d;[{啿B Y0Nlس/M3NJw͸_9:'69f̫P{s|.UFo@LC #"YZN:>/(M&''979}ybG8ppG,P{=~W-!{Cmh[8ql% ElYY71M|b*CP,6q:tٞ__FaȌEY]]-"asow] ob|o*y; !ڙA-3:Z(Ң= (sܺCGvdYD.\~#-ڇ# iy7Qrl 8af;/=֥sB@i_| S8w^jK򑮄MKno-Ed2YΜy}e\ȵ"xf! J 4_My qBEMrܹ"On/jbzh߯L@ ȉ\⵷ .NIG \|EnjĄbgYB3Ĉ;gF7LBXɭXVe9hrw1 Lo-IX__:2r"',cP$.%-2:4[vxϿp K/q-cs!| Dd Aٿ8KJxV2" !;#BtC총(--Յ?FIOXS".b~!Nhl,A6K/U(9w~OYtȈ0AOCT4d2EΝ?1[D D98v!Pz]yhkkE܊ "\o@^CFr x饗Mw~ދ ye9AKCt$#;iXi,9ǏQ:oKFHQN?ZpP ʶy-(:wx[>&2Bo@mهh;1\"燆,cko.m)rE"JBHEE%k]=떟*n)Zs.T(z-ɜ-Vu7hBN1+{P,bdfDZJhdi)[ZDDq4"\͌EsLTCjo(ഉ'G}:po-"V&^ጅe':R:XUR,2lgWlB%7n}W+#i}/+8} 9p|pȭl+%ڔi)BT#~C 7mr >s{͠XD1C݆ hRǏP1{"G\"*d.;y>n;~E] 犜 a>+B X6-iEhUb>8nfxNEZ[CY,re,= љe@u?XUWvWN 5vmg"|{@qN;H"GF,GbDYP@mG_cz مuv! F ?uΉʣGhGXw{ma!C Iܜ&/ ih mhCm@#`ԄDѺIENDB`viridian-1.2/AmpacheTools/XMLRPCServerSession.py0000755000175000017500000000430511511672746021471 0ustar charliecharlie#!/usr/bin/env python # -*- coding: utf-8 -*- ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE from SimpleXMLRPCServer import SimpleXMLRPCServer from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler import xmlrpclib import thread import socket """ XML RPC Server """ class RequestHandler(SimpleXMLRPCRequestHandler): # Restrict to a particular path. rpc_paths = ('/RPC2',) class XMLServer: def __init__(self, ip, port): self.ip = ip self.port = port self.is_running = False self.server = SimpleXMLRPCServer((ip, port), requestHandler=RequestHandler) self.server.register_introspection_functions() def serve_forever(self, data=None): """Start the server.""" if self.is_running == False: self.is_running = True thread.start_new_thread(self.__serve_forever, (None,)) def __serve_forever(self, data=None): """Helper to start in a thread.""" #self.server.socket.setblocking(0) #self.server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: self.server.serve_forever() except: pass def shutdown(self): """Shutdown and close the server.""" if self.is_running: self.server.server_close() self.server.socket.close() self.is_running = False def register_function(self, function, name=None): """Register a function to use with the XML RPC server.""" if name == None: self.server.register_function(function) else: self.server.register_function(function, name) if __name__ == "__main__": xml_server = XMLServer("localhost", 8000) xml_server.serve_forever() import time time.sleep(30) # server will run for 30 seconds viridian-1.2/AmpacheTools/dbfunctions.py0000755000175000017500000003113411511672746020247 0ustar charliecharlie#!/usr/bin/env python # -*- coding: utf-8 -*- ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE """ Extra functions to query the database, to be used by AmpacheTools.AmpacheGUI """ import cPickle def clear_cached_catalog(db_session): """Clear the locally cached catalog.""" try: c = db_session.cursor() tables = ["artists", "albums", "songs"] for table_name in tables: c.execute("""DELETE FROM %s""" % table_name) db_session.commit() c.close() except: return False return True def table_is_empty(db_session, table_name, query_id): """Check to see if a portion of the table is empty.""" try: c = db_session.cursor() if table_name == "albums": # albums c.execute("""SELECT 1 FROM %s WHERE artist_id = ? LIMIT 1""" % table_name, [query_id]) elif table_name == "songs": c.execute("""SELECT 1 FROM %s WHERE album_id = ? LIMIT 1""" % table_name, [query_id]) result = c.fetchone() #db_session.commit() c.close() if result != None: return False # not empty except: return True return True def song_has_info(db_session, song_id): """Check to see if a portion of the table is empty.""" try: c = db_session.cursor() c.execute("""SELECT 1 FROM %s WHERE song_id = ? LIMIT 1""" % 'songs', [song_id]) result = c.fetchone() #db_session.commit() c.close() if result == None: return False # not empty except: return False return True ########################################## # Functions to store artists/albums/songs ########################################## def populate_artists_table(db_session, list): """Save the list of artists in the artists table.""" if not list: # list is empty return False c = db_session.cursor() c.execute("""DELETE FROM artists""") for artist_list in list: c.execute("""INSERT INTO artists (artist_id, name, custom_name) VALUES (?, ?, ?)""", artist_list) db_session.commit() c.close() return True def populate_full_albums_table(db_session, list): """Save all albums to the albums table""" if not list: return False c = db_session.cursor() c.execute("""DELETE FROM albums""") for album_list in list: c.execute("""INSERT INTO albums (artist_id, album_id, name, year, precise_rating) VALUES (?,?,?,?,?)""", album_list) db_session.commit() c.close() return True def populate_albums_table(db_session, artist_id, list): """Save the list of albums in the albums table.""" if not list: # list is empty return False #print list c = db_session.cursor() c.execute("""DELETE FROM albums WHERE artist_id = ?""", [artist_id]) for album_list in list: c.execute("""INSERT INTO albums (artist_id, album_id, name, year, precise_rating) VALUES (?,?,?,?,?)""", album_list) db_session.commit() c.close() return True def populate_full_songs_table(db_session, list): """Save the list of songs in the songs table.""" if not list: # list is empty return False c = db_session.cursor() c.execute("""DELETE FROM songs""") for song_list in list: c.execute("""INSERT INTO songs (album_id, song_id, title, track, time, size, artist_name, album_name) VALUES (?,?,?,?,?,?,?,?)""", song_list) db_session.commit() c.close() return True def populate_songs_table(db_session, album_id, list): """Save the list of songs in the songs table.""" if not list: # list is empty return False c = db_session.cursor() c.execute("""DELETE FROM songs WHERE album_id = ?""", [album_id]) for song_list in list: c.execute("""INSERT INTO songs (album_id, song_id, title, track, time, size, artist_name, album_name) VALUES (?,?,?,?,?,?,?,?)""", song_list) db_session.commit() c.close() return True ########################################## # Public Getter Functions ########################################## def get_album_id(db_session, song_id): """Takes a song_id and returns the album_id""" c = db_session.cursor() c.execute("""SELECT album_id FROM songs WHERE song_id = ?""", [song_id]) result = c.fetchone()[0] #db_session.commit() c.close() return result def get_album_name(db_session, album_id): """Takes an album_id and returns the album_name""" c = db_session.cursor() c.execute("""SELECT album_name FROM albums WHERE album_id = ?""", [album_id]) result = c.fetchone()[0] #db_session.commit() c.close() return result def get_album_year(db_session, album_id): """Takes an album_id and returns the album_year""" c = db_session.cursor() c.execute("""SELECT year FROM albums WHERE album_id = ?""", [album_id]) result = c.fetchone()[0] #db_session.commit() c.close() return result def get_artist_id(db_session, album_id): """Takes an album_id and returns the artist_id""" c = db_session.cursor() c.execute("""SELECT artist_id FROM albums WHERE album_id = ?""", [album_id]) result = c.fetchone()[0] #db_session.commit() c.close return result def get_artist_name(db_session, artist_id): """Takes an album_id and returns the album_name""" c = db_session.cursor() c.execute("""SELECT name FROM artists WHERE artist_id = ?""", [artist_id]) result = c.fetchone()[0] #db_session.commit() c.close() return result def get_artist_ids(db_session): """Returns a list of all artist ID's.""" c = db_session.cursor() c.execute("""SELECT artist_id FROM artists""") list = [] for row in c: list.append(row[0]) #db_session.commit() c.close() return list def get_album_ids(db_session): """Returns a list of all album ID's.""" c = db_session.cursor() c.execute("""SELECT album_id FROM albums""") list = [] for row in c: list.append(row[0]) #db_session.commit() c.close() return list ####################################### # Public Dictionary Getter Methods ####################################### def get_artist_dict(db_session): """Returns a dictionary of all the artists populated from the database. This will check to see if the info exists locally before querying Ampache.""" artist_dict = {} try: c = db_session.cursor() c.execute("""SELECT artist_id, name, custom_name FROM artists order by name""") for row in c: artist_id = row[0] artist_name = row[1] custom_name = row[2] artist_dict[artist_id] = { 'name' : artist_name, 'custom_name' : custom_name, } except: artist_dict = None #db_session.commit() c.close() return artist_dict def get_album_dict(db_session, artist_id=None): """Returns a dictionary of all the albums from an artist from the database This will check to see if the info exists locally before querying Ampache.""" album_dict = {} if artist_id == None: try: c = db_session.cursor() c.execute("""SELECT album_id, name, year, precise_rating FROM albums""") for row in c: album_id = row[0] album_name = row[1] album_year = row[2] precise_rating = row[3] album_dict[album_id] = {'name' : album_name, 'year' : album_year, 'precise_rating' : precise_rating, } except: album_dict = None #db_session.commit() c.close() else: try: c = db_session.cursor() c.execute("""SELECT album_id, name, year, precise_rating FROM albums WHERE artist_id = ? order by year""", [artist_id]) for row in c: album_id = row[0] album_name = row[1] album_year = row[2] precise_rating = row[3] album_dict[album_id] = {'name' : album_name, 'year' : album_year, 'precise_rating' : precise_rating, } except: album_dict = None #db_session.commit() c.close() return album_dict def get_song_dict(db_session, album_id): """Returns a dictionary of all the songs from an album from the database This will check to see if the info exists locally before querying Ampache.""" song_dict = {} try: c = db_session.cursor() c.execute("""SELECT song_id, title, track, time, size, artist_name, album_name FROM songs WHERE album_id = ? order by track""", [album_id]) for row in c: song_id = row[0] song_title = row[1] song_track = row[2] song_time = row[3] song_size = row[4] artist_name = row[5] album_name = row[6] song_dict[song_id] = { 'title' : song_title, 'track' : song_track, 'time' : song_time, 'size' : song_size, 'artist_name' : artist_name, 'album_name' : album_name, } except: song_dict = None #db_session.commit() c.close() return song_dict def get_single_song_dict(db_session, song_id): """Returns a dictionary of one song based on its song_id""" song_dict = {} try: c = db_session.cursor() c.execute("""SELECT album_id, title, track, time, size, artist_name FROM songs WHERE song_id = ?""", [song_id]) for row in c: album_id = row[0] song_title = row[1] song_track = row[2] song_time = row[3] song_size = row[4] artist_name = row[5] song_dict = { 'album_id' : album_id, 'song_title' : song_title, 'song_track' : song_track, 'song_time' : song_time, 'song_size' : song_size, 'song_id' : song_id, 'artist_name' : artist_name, } c.execute("""SELECT name, album_id, precise_rating FROM albums WHERE album_id = ?""", [song_dict['album_id']]) data = c.fetchone() song_dict['album_name'] = data[0] song_dict['artist_id'] = data[1] song_dict['precise_rating'] = data[2] except: song_dict = None #db_session.commit() c.close() return song_dict def get_playlist_song_dict(db_session, song_id): """Returns a dictionary of one song with slightly less information (faster query).""" song_dict = {} try: c = db_session.cursor() c.execute("""SELECT title, artist_name, album_name FROM songs WHERE song_id = ?""", [song_id]) for row in c: song_title = row[0] artist_name = row[1] album_name = row[2] song_dict = { 'song_title' : song_title, 'artist_name' : artist_name, 'album_name' : album_name, } except: song_dict = None #db_session.commit() c.close() return song_dict def set_playlist(db_session, name, songs): """Saves a playilst with the given name in the database, automatically pickles list.""" c = db_session.cursor() c.execute("""DELETE FROM playlists WHERE name = ?""", [name]) c.execute("""INSERT INTO playlists (name, songs) VALUES (?, ?)""", [name, str(cPickle.dumps(songs))]) db_session.commit() c.close() def remove_playlist(db_session, name): """Removes a playlist from the database""" c = db_session.cursor() c.execute("""DELETE FROM playlists WHERE name = ?""", [name]) db_session.commit() c.close() def get_playlist(db_session, name, default_value=[]): """Retrieve a playlist from the database.""" try: c = db_session.cursor() c.execute("""SELECT songs FROM playlists WHERE name = ?""", [name]) result = c.fetchone()[0] c.close() except: c.close() return default_value return cPickle.loads(str(result)) def get_playlists(db_session): """Retrieve all playlists stored locally as a list""" c = db_session.cursor() c.execute("""SELECT name,songs FROM playlists""") list = [] for row in c: list.append( {'name' : row[0], 'songs': cPickle.loads(str(row[1])), } ) c.close() return list def create_initial_tables(db_session): """Create the tables in the database when the program starts""" c = db_session.cursor() c.execute('''CREATE TABLE IF NOT EXISTS artists (artist_id INTEGER NOT NULL DEFAULT '', name text NOT NULL DEFAULT '', custom_name text NOT NULL DEFAULT '', PRIMARY KEY (artist_id) ) ''') c.execute('''CREATE TABLE IF NOT EXISTS albums (artist_id int NOT NULL DEFAULT '', album_id int NOT NULL DEFAULT '', name text NOT NULL DEFAULT '', year int DEFAULT '', precise_rating int DEFAULT 0, PRIMARY KEY (artist_id, album_id) ) ''') c.execute('''CREATE TABLE IF NOT EXISTS songs (album_id int NOT NULL DEFAULT '', song_id int NOT NULL DEFAULT '', title text NOT NULL DEFAULT '', track int NOT NULL DEFAULT 0, time int DEFAULT 0, size int DEFAULT 0, artist_name text NOT NULL DEFAULT '', album_name text NOT NULL DEFAULT '', PRIMARY KEY (song_id) ) ''') c.execute('''CREATE TABLE IF NOT EXISTS playlists (name text NOT NULL DEFAULT '', songs text NOT NULL DEFAULT '', PRIMARY KEY (name) ) ''') db_session.commit() c.close() viridian-1.2/AmpacheTools/doc/gpl.txt0000644000175000017500000010451311511672746017446 0ustar charliecharlie GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . viridian-1.2/AmpacheTools/doc/DatabaseSession.html0000644000175000017500000001136011511672746022056 0ustar charliecharlie Python: module DatabaseSession
 
 
DatabaseSession
index
DatabaseSession.py

# -*- coding: utf-8 -*-
### BEGIN LICENSE
# Copyright (C) 2010 Dave Eddy <dave@daveeddy.com>
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.
### END LICENSE

 
Modules
       
cPickle
os
pysqlite2.dbapi2

 
Classes
       
DatabaseSession

 
class DatabaseSession
    A class to access and modify a sqlite database.
 
  Methods defined here:
__init__(self, database)
Initialize the database session and create a `variable` table.
commit(self)
Commits the database.
cursor(self)
Returns a cursor to the database.
table_is_empty(self, table_name)
Returns True if the table is empty.
variable_get(self, var_name, default_value=None)
Retrieve a variable from the database.
variable_set(self, var_name, var_value)
Save a variable in the database.

viridian-1.2/AmpacheTools/doc/AmpacheSession.html0000644000175000017500000005116611511672746021720 0ustar charliecharlie Python: module AmpacheSession
 
 
AmpacheSession
index
AmpacheSession.py

# -*- coding: utf-8 -*-
### BEGIN LICENSE
# Copyright (C) 2010 Dave Eddy <dave@daveeddy.com>
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.
### END LICENSE

 
Modules
       
datetime
hashlib
os
re
socket
time
urllib
urllib2
_xmlplus

 
Classes
       
AmpacheSession

 
class AmpacheSession
    The AmpacheSession Class.  This is used to communicate to Ampache via the API.
 
  Methods defined here:
__init__(self)
Initialize an AmpacheSession.
authenticate(self)
Attempt to authenticate to Ampache.  Returns True if successful and False if not.
This will retry AUTH_MAX_RETRY(=3) times.
get_album_art(self, album_id)
Takes an album_id and returns the url to the artwork (with the current authentication).
get_albums_by_artist(self, artist_id)
Gets all albums by the artist_id and returns as a list of dictionaries.
Example: [
                {        'artist_id'      : artist_id,
                         'artist_name'    : artist_name,
                         'album_id'       : album_id,
                         'album_name'     : album_name,
                         'album_year'     : album_year,
                         'album_tracks'   : album_tracks,
                         'album_disk'     : album_disk,
                         'album_rating'   : album_rating,
                         'precise_rating' : precise_rating,
                },
                { ... },
         ]
get_artists(self, offset=None)
Gets all artists and return as a list of dictionaries.
Example: [
                { 'artist_id' : artist_id, 'artist_name' : artist_name},
                { 'artist_id' : 1, 'artist_name' : 'The Reign of Kindo'},
                { ... },
         ]
get_credentials(self)
Retrun the url, username, and password as a tuple.
get_last_update_time(self)
Returns the last time the catalog on the Ampache server was updated.
get_playlist_songs(self, playlist_id, re_auth=False)
Gets all info about a song from the song_id and returns it as a dictionary.
Example: [ 
                {       'song_id'        : song_id,
                        'song_title'     : song_title,
                        'artist_id'      : artist_id,
                        'artist_name'    : artist_name,
                        'album_id'       : album_id,
                        'album_name'     : album_name,
                        'song_track'     : song_track,
                        'song_time'      : song_time,
                        'song_size'      : song_size,
                        'precise_rating' : precise_rating,
                        'rating'         : rating,
                        'art'            : art,
                        'url'            : url,
                 },
                 {...}
        ]
get_playlists(self, re_auth=False)
Gets a list of all of the playlists on the server.
Example: [
                {        'id'      : id,
                         'owner'   : owner,
                         'name'    : name,
                         'items'   : items,
                         'type'    : type,
                },
                { ... },
         ]
get_song_info(self, song_id)
Gets all info about a song from the song_id and returns it as a dictionary.
Example: {      'song_id'        : song_id,
                'song_title'     : song_title,
                'artist_id'      : artist_id,
                'artist_name'    : artist_name,
                'album_id'       : album_id,
                'album_name'     : album_name,
                'song_track'     : song_track,
                'song_time'      : song_time,
                'song_size'      : song_size,
                'precise_rating' : precise_rating,
                'rating'         : rating,
                'art'            : art,
                'url'            : url,
         }
get_song_url(self, song_id)
Takes a song_id and returns the url to the song (with the current authentication).
get_songs_by_album(self, album_id)
Gets all songs on album_id and returns as a list of dictionaries.
Example: [
                {       'song_id'        : song_id,
                        'song_title'     : song_title,
                        'artist_id'      : artist_id,
                        'artist_name'    : artist_name,
                        'album_id'       : album_id,
                        'album_name'     : album_name,
                        'song_track'     : song_track,
                        'song_time'      : song_time,
                        'song_size'      : song_size,
                        'precise_rating' : precise_rating,
                        'rating'         : rating,
                        'art'            : art,
                        'url'            : url,
                },
                { ... },
         ]
has_credentials(self)
Checks to see if the AmpacheSession object has credentials set.
is_authenticated(self)
Returns True if self.auth is set, and False if it is not.
ping(self)
Ping extends the current session to Ampache.
Returns None if it fails, or the time the session expires if it is succesful
set_credentials(self, username, password, url)
Save the ampache url, username, and password.

 
Data
        AUTH_MAX_RETRY = 3
DEFAULT_TIMEOUT = 5
viridian-1.2/README0000644000175000017500000000143211511672746013653 0ustar charliecharlie=== Viridan === Viridian is a front-end for an Ampache server (see http://ampache.org). Viridian is powered by python/pygtk. === Installing === To use: you can run ./viridian to run it from the current directory, or you can install it using: ~# python setup.py install === Problems === If you have used Viridian before and found that an update has messed it up, open Viridian and go to Edit -> Preferences -> System and hit Reset Everything. You can also do this manually by executing ~$ rm -r ~/.viridian === More Information === Home Page: http://viridian.daveeddy.com Launchpad: https://launchpad.net/viridianplayer FAQ: https://answers.launchpad.net/viridianplayer/+faqs Bugs: https://bugs.launchpad.net/viridianplayer Questions: https://answers.launchpad.net/viridianplayer viridian-1.2/ViridianApp.png0000644000175000017500000003004211511672746015706 0ustar charliecharliePNG  IHDRmM˭>tEXtSoftwareAdobe ImageReadyqe</IDATxiwGҾ HHBl{9 `̼zz=t`a6b l ?K)J\""2#3LpSU*EF>cBCEJӧ]Û @a]OkP6]?(J;K!$wbP!CRW Nm7Š,1Ķ!nGZ%ƅ}䢇-w(Pr}$d!5b1QL<ϬF1@P;>P!wm\pl>PX xl' ̃6!/<55bA (B7S.j>v*V=@@Xߡ(@mwU^`#L;P@5V=@55kzkj!rX!qX!q8B⮱B}_OS~rwA.PGu^` ]W7] Dvr`-fn_GMw N6&.t,Xߢslbb‹hl]y0{:,xw,S`~`CC10Sc/A DT/C|E^SiTMw=wz<N27Uoe*`leh54yF/s[(z&*~1 D|VA_aKKKUeA8׿acccaJ"m:{% IJ}{62r3,q)KJZꂺ5618L_[X[ ޼eC֭vmC26{4(twF3` zMCDx@ dB"}٪Xą.8ڐ0X\( s>r[0(QȍaB2"!nɉ(:L͠ !Pr[#ie6ETX_#=AJ8($#G8mZF2($e7h\@&bjzD`˽{cƍlCnF^d$D|TD@,@Hot)DDS$Q6˜FslB@1Kzn}޽?g! XԿD嗁hOuv4L, bC^c!\bGhQ DI6:zG_zsFY7h߿<6Sxpj[<E&*6X"JarjRVĠDCNMS%?% E. @Q(xv5x7h # @Ȅ&j,/# }@]5ExܜbQ6T$jE. / ҬIM=/ K,{dm?LywYCCv5%WŵMbyϥ@( Jh3}_7WZ’c.;>+eAdȆhT,gm#4 'N&BsqFL?wccC?e}}c[ |'R{d m'Emm-vGi[^|cc-PX"N5S@tb(s^򨠂+fP9vBdQCuA% d(n61 ,{ds" z2^*ha"| @ Ic<:pTFzlaN&'r۞BAe=:LhWEb@,/O$ɦvL9K(=WӇB\VH-L@"o Pfkn6F"Q 8:Lh!)l^"mB< *t/>xQEb\6 *(&(O ! C(Ǟ6dSHtѶ\[|B`4Tml @цdYI666崡@rڅ96m6PiOa'>??# q:t dv"J*b? і괣:~c~^ -Uu0l-BA4*ؓ'OP6:xoֽm(D;C'Ӷj}= ?m+քr1PmH<( n45cl 2EkPy -Һwoh? yB$G0 hzzYAA oNd!OرxI4)+++CuڈG@- "]`ii|!iCA`1W̓ lNׯB < z*^Ɠm^IuUxe;mH65F1"ڿa-S5bkEg4i٢G%4 kMR,±H2d---Z#M:|^@:,z^hY=3Qlm8fP,,MeCAԡ}ٶܹ hD;xĭBDq|ݽ %i]y(`цiPVV\T:#>6t(_x$P,Bd;Q^^3VDFš9m)yc]FN ͿQ4͢TnhԙI@9,#^T u446m/ ) ęgb}]ұH'&&XֺtO2=3PeB Ά"{,b/}lcc>|4"*T5dMMMc"6q=}0ȑ8&|h\4lbS,5+TyG{>2NPReϠ^(ME4ؖյ!jOrk3Dn:Y G,RF"#/X__?+++GD\C{8D[h嫿6cm-G*.b6ԻkX ˾2 "XD-_ENMNyӧOYmm))w M_پɳ6rS} nO@Ul(C40gHE@EU%%%4HI97 N>@E-''іha LRN,Y__(#Oi؞NđEZr+gh}\{Ϟ} ;[[[FE|so&"rڲ_>dMMM/.-ܐH .a,)B%`VUƔEݘJQs[T:"m~155ŊKmSTB {j:nΠ=) hP{9ˎy-b6=*Rޮ_\ZJ( a'KY[fpv`\ ~C ֐Qu-n7e/cɺ$IeI~.AteL;,h7q?hx jk`EXH"]TTmel⋘{βaWGk*E4VUUʪjC0vPHZNŜ'%~ R-?[$`ȹ? f\S}X.ή 9v_g9miE$$$ܓ 'Ǎ.ܾ= A2&gTn" Zhdm}MNM'@ G;&''PTzX#pNG9MՒY]%JEqcc?`!d^"Nc|cuM]]Y|mSa&rm ;CaiP3Jbvv&E=ru'(1WH5Y޽91VWWw#;1lc(Qwoe˄"LprU) V j-i>\OګO+J3== n L*|^?ECEx5[449jr@1diiIwtI$/_ӈhLgzz:p ʲ3D7^lXJFD)++=B%ڙО tQBM9.%L*#-}'Ao;?8VJNuuGєD|r䴁XcssC3{ih{qp'MQsUhF C=l9u}qqQ?tHkPpm袙)?`79In%=dI?\ R1ޫ>rT?I{TWװ=-zOuOTFvՐqE' ]z!'jz\fd2QټvCq}&]\RB*v&ƼTzHʹhD*z2YcjKܹB`us[R=O^:X1V B1 ef[XXݶ` Vh^xx@e#"fP,woCX?~`ǏKpH$왐xetx֭X 3iD2#mr۴•CW3795Ɵ :A!ʱ1[3ShVni"29s#z1][f:® %g oXZsvZ'}mCl%Dgw;welLh0 Q6svD,b/׮}˝ eX@dyy,M+$Zl1"bٻ볎RnhKbjj8"Z򽾱 }hSY~ n`al#oD{{00P 6tB]mg}Ui:|UmP,Һw]e- R? D;vМٙafgX=BorkZm vVSSkU==TCp۰˥!@#۶۲ueeOEq3C1G}\yutvdңP&<]C}޾ 5 Z|iܓt"}}spX= L35mmmZ,E WW!zO -:ՆZAm W@s͚"ho`XFNS<*,}7W/7Նh HNY__cY̽6cum}? El_Ol;AJI۽ϫϵ7Ɇh tttZkX2)xD}x+dz/{BG*6>te&lI}2. ՆhHH![ .#pˑׯ Z)[?pN ;q&mP]ms>ʲ~dsz>t 7M0dtvCgW728VH5Ey W B#):m_37;kz߳>![[[ML__?WLuwm_k:OFCFmD$Oشkl$iZY7]0tvvzGcyy 9-r}v^g|g( H\yr&hX1 {UwΥ7 Bf] Gl؋$!T<8+,t>ȹB FN5|]sy/wv@ іll;m;flh{ۋ$[vi_& Yga=vL_D(ƹ{D5@UhGj8+Y"Ͼ mK71XD##i{iIIٚ}zXee J{Q役cjCeAKrO>Յ8ݴ'(?x7F3fX߳ ^T@{a2ḇqT9^GgC!ڒX\\dk54{kĺHg;m P:n.[ԎoE^ ހן ɆhKeVsۻmNܞ_pH\ cp>:nX8x3EWo\Sw#2\nojCe259v7XҲwЎ 7!'Xaa㍡T6Bູ'+{Wm^6D["ssu!+..&w> ЀU/*~c,'4R+Ύ"GՃhdccC4YD)v[ʕ/ǍLii);xdϽEYC`n\\CцhG?F4=d~r7@'Nb['XxܿB262"l~"CC4mE{f6Ol6oh@q{ Ev *\D\#m_S5ce~9G(| ȡÇ{F$@<9Wۦp/m{UVUY&MB~(t nh ul*sed++(p #G"iy8h@;hw4Z x A@.hKymq t6uk-gENXnp%loZTC hC}n="tдyS>g. YF,?khhz 7sp9 F}b h4ɵf==+++2u9H;z(kaWg1Jy›xjT //,v6vv<x<"lG,r,I\?GTCDo -5nnrr;r;$ :Ԡ!CXYDjhs]Ȇ`{#|C ¡}Hh=͡ffٞ\{ffCJFϧy鍣x&=!ު4Ͼ eUmjJJJRE\?o<7wNNq9xTIz,͆h* >s nj}?P8vԘ d]!iY)-!lDBi`("itY޽'(L XsxUZ<8*H@<ʩ-..XvWVٯ+X.WH>"hK Qn5{#4D lD{q+ؒg\=u4+**.@[)цh VX;(@iH?k{z{m%J3UQ LOMk"Qk;r\~$s\+(9q r>W8n/ 2m,,;v(6DSo% NV,?j8c8PE6b;2gq,B hj_HE4Ez{ !A7-G_~U7(!=K}ݹs&sz,z,rv b8#4[h8q;lz nی tiuuۭʢM,l,Civ~f0 d;[{啿B Y0Nlس/M3NJw͸_9:'69f̫P{s|.UFo@LC #"YZN:>/(M&''979}ybG8ppG,P{=~W-!{Cmh[8ql% ElYY71M|b*CP,6q:tٞ__FaȌEY]]-"asow] ob|o*y; !ڙA-3:Z(Ң= (sܺCGvdYD.\~#-ڇ# iy7Qrl 8af;/=֥sB@i_| S8w^jK򑮄MKno-Ed2YΜy}e\ȵ"xf! J 4_My qBEMrܹ"On/jbzh߯L@ ȉ\⵷ .NIG \|EnjĄbgYB3Ĉ;gF7LBXɭXVe9hrw1 Lo-IX__:2r"',cP$.%-2:4[vxϿp K/q-cs!| Dd Aٿ8KJxV2" !;#BtC총(--Յ?FIOXS".b~!Nhl,A6K/U(9w~OYtȈ0AOCT4d2EΝ?1[D D98v!Pz]yhkkE܊ "\o@^CFr x饗Mw~ދ ye9AKCt$#;iXi,9ǏQ:oKFHQN?ZpP ʶy-(:wx[>&2Bo@mهh;1\"燆,cko.m)rE"JBHEE%k]=떟*n)Zs.T(z-ɜ-Vu7hBN1+{P,bdfDZJhdi)[ZDDq4"\͌EsLTCjo(ഉ'G}:po-"V&^ጅe':R:XUR,2lgWlB%7n}W+#i}/+8} 9p|pȭl+%ڔi)BT#~C 7mr >s{͠XD1C݆ hRǏP1{"G\"*d.;y>n;~E] 犜 a>+B X6-iEhUb>8nfxNEZ[CY,re,= љe@u?XUWvWN 5vmg"|{@qN;H"GF,GbDYP@mG_cz مuv! F ?uΉʣGhGXw{ma!C Iܜ&/ ih mhCm@#`ԄDѺIENDB`viridian-1.2/viridian0000755000175000017500000000574111511672746014535 0ustar charliecharlie#!/usr/bin/env python # -*- coding: utf-8 -*- ### BEGIN LICENSE # Copyright (C) 2010 Dave Eddy # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE __program__ = "Viridian" __author__ = "David Eddy" __credits__ = ["Michael Zeller", "Skye Sawyer"] __license__ = "GPLv3" __version__ = "1.2" __maintainer__ = "David Eddy" __email__ = "dave@daveeddy.com" __status__ = "Release" import AmpacheTools import gettext import gobject import dbus, dbus.service, dbus.glib import os VIRIDIAN_DIR = os.path.expanduser("~") + os.sep + '.viridian' LOCALES_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)) , 'locales') class AmpacheService(dbus.service.Object): """Used for single instance""" def __init__(self, app): self.app = app bus_name = dbus.service.BusName('com.daveeddy.Viridian', bus = dbus.SessionBus()) dbus.service.Object.__init__(self, bus_name, '/com/daveeddy/Viridian') @dbus.service.method(dbus_interface='com.daveeddy.Viridian') def show_window(self): """Used to bring the current window to the front""" self.app.window.present() if __name__ == "__main__": # initialize gettext gettext.install("viridian", LOCALES_DIR) try: # try to see if an instance is already running, and if so don't make a second instance if dbus.SessionBus().request_name("com.daveeddy.Viridian") != dbus.bus.REQUEST_NAME_REPLY_PRIMARY_OWNER: print _("Application already running.. exiting.") method = dbus.SessionBus().get_object("com.daveeddy.Viridian", "/com/daveeddy/Viridian").get_dbus_method("show_window") method() else: raise Exception(_('Not Running')) except: # some distros don't support this, so just open a second instance i guess.... is_first_time = False if not os.path.exists(VIRIDIAN_DIR): is_first_time = True os.mkdir(VIRIDIAN_DIR) os.chmod(VIRIDIAN_DIR, 0700) db_session = AmpacheTools.DatabaseSession(os.path.join(VIRIDIAN_DIR, 'viridian.sqlite')) ampache_conn = AmpacheTools.AmpacheSession() audio_engine = AmpacheTools.AudioEngine(ampache_conn) # create the gui and give it access to all of the components it needs gui = AmpacheTools.AmpacheGUI(ampache_conn, audio_engine, db_session, is_first_time, __version__) # this is for dbus and single instance service = AmpacheService(gui) # allow the audioengine to callback to the GUI (for messages like End Of Song) audio_engine.set_ampache_gui_hook(gui) # display the gui gui.main() viridian-1.2/doc/gpl.txt0000644000175000017500000010451311511672746015067 0ustar charliecharlie GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read .