Compare commits

...
313 Commits
Author SHA1 Message Date
Ade e9d9a6c441 misaligned else 2014-09-20 11:44:34 +12:00
Ade 8838ee481e Album searchresults CAA
tryCAA not working for me, added this.onerror=null instead to stop
looping
2014-09-20 11:41:03 +12:00
AdeHub 0fd4c9c998 Merge pull request #1892 from basilfx/libav
Add support for Libav-tools
2014-09-18 16:52:37 +12:00
Bas Stottelaar b857108db4 Add support for libav-tools 2014-09-18 02:13:17 +02:00
Bas Stottelaar 16ec910ba5 Initialize logging only for multiprocessing 2014-09-17 15:42:08 +02:00
Bas Stottelaar 5e98d96f65 Add proper support for logging in multiprocessing environment. Useful for multicore encoding. 2014-09-17 13:10:31 +02:00
AdeHub 7e2b9a30a2 Merge pull request #1890 from basilfx/freezedb
Add the option to freeze the database during post processing
2014-09-17 19:07:43 +12:00
AdeHub 4d683922c1 Merge pull request #1889 from basilfx/cleanup
Major cleanup of templates and code + delayed loading of album art
2014-09-17 19:07:16 +12:00
Bas Stottelaar 9d594988d9 Show artist ID, for debugging with multiple artist ids 2014-09-17 03:24:04 +02:00
Bas Stottelaar c441e888c2 Add option to freeze database during post processing 2014-09-17 03:16:43 +02:00
Bas Stottelaar 298daf9895 Don't forget the upcoming albums 2014-09-17 01:54:37 +02:00
Bas Stottelaar d04dff9130 Don't keep retrying CCA image 2014-09-17 01:51:03 +02:00
Bas Stottelaar 5eef5a594c Implemented unveil plugin. Load images only when shown on screen 2014-09-17 01:38:49 +02:00
Bas Stottelaar cce98709a7 Upgraded all JavaScript libraries. Moved UI parts from default interface to global. Fixed JavaScript Errors 2014-09-17 00:50:23 +02:00
Bas Stottelaar 3fef151fca Sorted all JavaScript, image and style files. Basically, it drops support for IE6/7 2014-09-16 22:43:20 +02:00
Bas Stottelaar a6fe517f87 Removed import lib.*/from lib.* 2014-09-16 22:06:41 +02:00
AdeHub 964c5008ca Merge pull request #1887 from basilfx/searchfix
Fixes for TPB and KAT search engine
2014-09-16 23:02:49 +12:00
Bas Stottelaar f250ae58a4 Fixes for search engines 2014-09-16 02:46:09 +02:00
AdeHub 7af194822c Merge pull request #1883 from basilfx/improvements2
Again, a set of improvements
2014-09-15 17:16:17 +12:00
AdeHub c4b1f3f4fe Merge pull request #1882 from basilfx/titles
Add titles to HTML tags in settings
2014-09-15 17:15:52 +12:00
Bas Stottelaar f4b13533ce Add support for Subsonic library update. Fixes #1864 2014-09-15 01:29:40 +02:00
Bas Stottelaar 638baf564d Convert old-style notifier classes to new-style classes 2014-09-15 00:54:30 +02:00
Bas Stottelaar 21b3d99249 Debug JSON responses 2014-09-15 00:40:32 +02:00
Bas Stottelaar d8dda5b24f First go on adding documentation to the title attribute. Not fully completed, but a first attempt. References #1803 2014-09-15 00:23:17 +02:00
Bas Stottelaar 25fb4361dc Code formatting. 2014-09-14 23:24:19 +02:00
Bas Stottelaar 8ec1808313 Another batch of improvements:
* Prefer 'with' statements for open
* Catch proper exceptions
* Remove ex() method, there was only one use and we catch thousands of exceptions.
2014-09-14 23:14:29 +02:00
Bas Stottelaar 43efca9d04 Code improvements. 2014-09-14 22:58:19 +02:00
Bas Stottelaar 34b9480a1b pyItunes is not used anymore. 2014-09-14 22:58:05 +02:00
Bas Stottelaar 51dfeb8190 myDB already available. Seems unneccesary to open another connection 2014-09-14 22:54:45 +02:00
Bas Stottelaar 3a03d940a1 Code improvements. Catch proper exceptions 2014-09-14 22:54:08 +02:00
Bas Stottelaar 69268b7d21 Proper line breaks for long lines 2014-09-14 22:41:07 +02:00
Bas Stottelaar e69cf3942e Increased logging for Last.FM requests 2014-09-14 22:36:30 +02:00
Bas Stottelaar 2e6c8e6537 Code improvements to the cache
* Catch the right exceptions where possible
* Move class vars to instance vars
* Prefer 'with' statement for files
2014-09-14 22:26:20 +02:00
Bas Stottelaar ca084f1216 It's more than that :-) 2014-09-14 22:18:35 +02:00
Bas Stottelaar a1bc3617a4 More logging + proper exception catching. 2014-09-14 14:54:39 +02:00
Ade a1e2042315 torrent more logging 2014-09-12 12:07:15 +12:00
Ade 3bb173c979 Piratebay user agent + More torrent removal stuff
- Piratebay user agent
- Torrent removal search interval config + more logging
2014-09-12 11:53:58 +12:00
Pieter Janssens 70135a5bf4 Merge pull request #1853 from basilfx/improvements
Improvements
2014-09-03 23:26:51 +02:00
Bas Stottelaar efa9ac77b8 Added link to GitHub and Troubleshooting in the footer, on the left side 2014-09-03 15:38:53 +02:00
Bas Stottelaar e8b330f3ba Strip message for debug logging. Truncated lines 2014-09-03 15:26:20 +02:00
Bas Stottelaar 56adf4a1ee Added log.info() for writing config to file 2014-09-03 15:18:49 +02:00
Bas Stottelaar 9f92b3b774 Improved Headphones.py 2014-09-03 14:54:30 +02:00
Bas Stottelaar 9d774b4486 Improved webstart.py
* Exit with code 1 on port in use
* Log when authentication is enabled
* Cleanup of code
2014-09-03 14:54:30 +02:00
Bas Stottelaar 163e1e3f2e Removed simplejson since that is Python 2.5 2014-09-03 14:54:30 +02:00
Ade adaaf51e9a Normalize path replacement strings
Potential fix for https://github.com/rembo10/headphones/issues/1824
2014-09-03 22:35:47 +12:00
Ade 2ba5978f84 Merge branch 'master' into develop 2014-09-02 19:42:15 +12:00
Ade 46240fb7d1 Add Demo to Extras
- fix up songkick location breaking artist page
- add description and link for songkick metro area filter
2014-08-30 22:17:30 +12:00
Ade a4fa8ca79d Reverted PP 2014-08-28 19:10:22 +12:00
Ade 701f709c23 Revert "Creating two functions from duplicated code"
This reverts commit 59b4cefd53.

Conflicts:
headphones/postprocessor.py

Reverting PP changes
2014-08-28 18:12:24 +12:00
AdeHub 3d22a66701 Merge pull request #1839 from rembo10/develop
Merge develop
2014-08-27 20:51:17 +12:00
Pieter Janssens a401ff4fb4 Merge pull request #1837 from multikatt/init_cleanup
Long list of calls -> for loop
2014-08-27 09:33:46 +02:00
Pieter Janssens 72419227e9 Merge pull request #1823 from multikatt/postproc_cleanup
Create two new functions, lowering amount of duplicated code
2014-08-27 09:33:02 +02:00
Ade 22bff59789 More Extras
- Artist Refresh, if removing extras from Modify/Fetch Extras then also
remove from db
- mb, use secondary type if type = Album and different, e.g DJ-mix
- utorrent - only check “Put new downloads in” directory
2014-08-27 18:59:31 +12:00
David b3746dc58e Long list of calls -> for loop 2014-08-26 20:20:21 +02:00
Ade b950bcbd0b Extras changes
- Append secondary type to type description if different, e.g. Album +
Mixtape/Street
- Keep Extras in order
- Fix bug where albums not deleted from db if not in mb results when
refreshing Artist
2014-08-25 18:46:09 +12:00
Ade aa6206a235 Remove from have when deleting Album 2014-08-23 09:08:48 +12:00
AdeHub 8cf23b79bf Merge pull request #1828 from multikatt/minor
Remove unused import
2014-08-19 22:17:50 +12:00
Ade bb884eeab7 Duplicate track issue
Possible fix for https://github.com/rembo10/headphones/issues/1504
2014-08-19 21:49:03 +12:00
David 72d2201dd6 Remove unused import 2014-08-18 21:17:28 +02:00
David 1b65df6c45 Return false on non existing dirs 2014-08-17 19:48:47 +02:00
Ade a7d1f128ad Create alltracks when adding album
Potential fix for https://github.com/rembo10/headphones/issues/1586
2014-08-17 18:14:18 +12:00
David 17ef29887e Use _file instead of file
since file is a built-in function
2014-08-16 01:57:05 +02:00
Ade 06822e2514 Ignore Artist fix
Fixes https://github.com/rembo10/headphones/issues/1660
2014-08-16 10:49:42 +12:00
Ade fc1c26f58f Sanitize name too
Fixes https://github.com/rembo10/headphones/issues/1706
2014-08-16 09:55:37 +12:00
David ef3cd00ef1 Removing duplications, replacing with functions 2014-08-15 19:12:02 +02:00
David 08f483d77d Removing duplications, replacing with functions 2014-08-15 18:57:44 +02:00
David 405ebe5ee2 Removing duplications, replacing with functions 2014-08-15 17:46:01 +02:00
David 59b4cefd53 Creating two functions from duplicated code 2014-08-15 17:02:34 +02:00
Ade d037147c23 Manage Unmatched fix
Potential fix for https://github.com/rembo10/headphones/issues/1808
2014-08-15 20:16:35 +12:00
Pieter Janssens 34097e2d81 Merge remote-tracking branch 'origin/develop' 2014-08-14 13:45:54 +02:00
Pieter Janssens e0f51f756c Merge pull request #1820 from basilfx/bugfixes7
Another set of small bug fixes and improvements
2014-08-14 09:19:21 +02:00
Bas Stottelaar 8c9b834bc4 Check for total number of tracks before continuing. Prevents ZeroDivisonError's 2014-08-13 18:31:08 +00:00
Bas Stottelaar 9178740c26 Added license info to requests.py 2014-08-13 18:24:17 +00:00
Bas Stottelaar 922cca9652 Constants should be in caps 2014-08-13 18:21:51 +00:00
Bas Stottelaar e2f0354deb Raised Last.FM timeout to 60 seconds 2014-08-13 18:20:29 +00:00
Ade 6891252cf6 utorrent fix up 2014-08-13 18:14:11 +12:00
Pieter Janssens c9debdcce5 Merge pull request #1812 from piejanssens/Begall-db-fixes
Begall's db fixes
2014-08-11 20:52:50 +02:00
piejanssens 6911dfde02 Merge branch 'develop' into Begall-db-fixes 2014-08-11 20:51:35 +02:00
piejanssens e7ddb2d7b2 Resolved conflicts for Begall's db fixes 2014-08-11 20:47:57 +02:00
Pieter Janssens 51fde7a52b Merge pull request #1811 from piejanssens/Begall-renamer-origfolder
Add $OriginalFolder flag to renamer (thx Begall)
2014-08-11 20:38:15 +02:00
piejanssens b36178d6d4 Add $OriginalFolder flag to renamer (thx Begall)
Add $OriginalFolder flag to renamer #1699
2014-08-11 20:37:09 +02:00
Pieter Janssens c19dffcb46 Merge pull request #1810 from piejanssens/develop
Add (re)search to wanted album on artist page
2014-08-11 20:24:07 +02:00
piejanssens e04245cff5 Add (re)search to wanted album on artist page
If the snatch got lost, you can click search instead of skip and then
search.

Replaces #1261
2014-08-11 20:23:00 +02:00
Pieter Janssens d5b08a8f51 Merge pull request #1809 from piejanssens/nzbget-priority
Add NZBget priority option (including force)
2014-08-11 20:12:39 +02:00
piejanssens 49179170fa Add NZBget priority option (including force)
This feature allows the user to select a priority for the nzb that is
sent to nzbget. Force will download the task even though the queue is
paused.
2014-08-11 20:09:19 +02:00
Ade e224ad1a6a Torrent seed ratio part 2
Set scheduled job to remove torrent when post processed and finished
seeding
2014-08-11 20:32:37 +12:00
Ade ba552bd1f8 Torrent seed ratio 2014-08-08 22:23:06 +12:00
Pieter Janssens ad1de823bd Merge pull request #1797 from Begall/nfo-orig
Add ability to rename .nfo file to .nfo-orig
2014-08-07 08:49:53 +02:00
Begall f2f8ad68a3 Merge branch 'develop' into nfo-orig 2014-08-06 23:19:26 +01:00
Begall 23e3dd91b3 Add ability to rename .nfo file to .nfo-orig 2014-08-06 22:53:41 +01:00
Pieter Janssens b4df21776e Merge pull request #1795 from rembo10/develop
Update README.md
2014-08-06 22:05:12 +02:00
Pieter Janssens 741538fa3c Update README.md 2014-08-06 22:04:49 +02:00
piejanssens 270d32b45e Merge remote-tracking branch 'origin/develop' 2014-08-06 21:44:36 +02:00
Pieter Janssens cedf54865d Merge pull request #1794 from multikatt/typo
small typo: ebedding -> embedding
2014-08-06 20:04:37 +02:00
piejanssens c0199cf356 Merge branch 'Fix-for-artist-name-in-album-title' into develop 2014-08-06 20:01:46 +02:00
piejanssens c95bb9d29c Check for album's with artist in title
Fixes #1788 and #1666
2014-08-06 19:59:59 +02:00
David 29ae400b80 small typo: ebedding -> embedding 2014-08-06 19:01:01 +02:00
Pieter Janssens 0cd4d540aa Merge pull request #1793 from multikatt/cleanup
Cleaning up redundant code and long lines
2014-08-06 17:57:40 +02:00
David 6ce710dbb1 Cleaning up redundant code and long lines 2014-08-06 17:50:17 +02:00
Pieter Janssens 90744c3b80 Merge pull request #1791 from multikatt/end-of-line
End of line convertion
2014-08-06 17:31:44 +02:00
Pieter Janssens d777cf74a5 Merge pull request #1792 from multikatt/gitignore
gitignore vims .swp files
2014-08-06 17:31:38 +02:00
David 9b1d6eb6e0 gitignore vims .swp files 2014-08-06 17:25:49 +02:00
David 676fa7199b Adding auto conversion to lf in gitattributes
To make end-of-lines consistent in the future.

Quoting http://git-scm.com/docs/gitattributes:
    When text is set to "auto", the path is marked for automatic
    end-of-line normalization. If Git decides that the content is text,
    its line endings are normalized to LF on checkin.
2014-08-06 17:02:08 +02:00
David fd30b3501e Converting crlf to just lf
Making end-of-lines consistent throughout the codebase.
2014-08-06 17:00:29 +02:00
Pieter Janssens 2a601d2731 Merge pull request #1784 from multikatt/1743
Replace Add with Search on top
2014-08-06 16:00:03 +02:00
David 296c9ca767 Reducing search bar width 2014-08-06 15:55:08 +02:00
Pieter Janssens 245f1fe366 Merge pull request #1787 from multikatt/cleanup
Creating method of shutdown actions
2014-08-06 14:04:27 +02:00
Pieter Janssens ce3b8b7894 Merge pull request #1789 from multikatt/drop_whitespace
Dropping trailing whitespaces
2014-08-06 13:28:51 +02:00
David 0c61eb11f5 Dropping trailing whitespaces 2014-08-06 12:46:26 +02:00
David 9ec4866cea renaming do_shutdown() -> do_state_change() 2014-08-06 12:10:03 +02:00
David d48dbc6bbb Creating method of shutdown actions 2014-08-06 00:59:03 +02:00
piejanssens 5cb9d78e35 Merge branch 'develop' 2014-08-05 23:05:32 +02:00
Pieter Janssens 78c866ce59 Update README with IRC instructions
We have an IRC channel now!
2014-08-05 21:36:25 +02:00
Pieter Janssens ab6fcc92e5 Merge pull request #1783 from multikatt/lyrics
Actually check if lyrics are found
2014-08-05 20:57:52 +02:00
Pieter Janssens ba640e2659 Merge pull request #1786 from piejanssens/develop
Added additional extra's for albums (e.g. dj-mix or mixtape/street).
2014-08-05 20:55:05 +02:00
piejanssens 9849a8334f Added additional extra's for albums (e.g. dj-mix or mixtape/street). 2014-08-05 20:51:48 +02:00
David ad645d960d Replace Add with Search on top 2014-08-05 18:28:34 +02:00
David 98604a2ab8 Make embedLyrics() actually check if lyrics found
Checking metalyrics, the variable, not lyrics the module
2014-08-05 17:10:48 +02:00
David bd8da45c1a Replacing redundant code with variable 2014-08-05 17:07:55 +02:00
Pieter Janssens b46787ae93 Merge pull request #1782 from multikatt/proxy
Creating set_proxy, which generates proxy urls
2014-08-05 14:14:54 +02:00
David 48b2b9235d Creating set_proxy, which generates proxy urls
Making it a function reduces the amount of repetition.
2014-08-05 13:57:02 +02:00
Pieter Janssens efd2e44ded Merge remote-tracking branch 'origin/develop' 2014-08-05 13:05:46 +02:00
Pieter Janssens 93e4e47cd6 Update gitignore (pycharm) 2014-08-05 13:04:33 +02:00
Pieter Janssens 9ec65c5f9a Fix KAT proxy slide in/out 2014-08-05 13:02:41 +02:00
Pieter Janssens 1597d028f0 Merge remote-tracking branch 'origin/develop' 2014-08-05 10:38:26 +02:00
Pieter Janssens 2178397c2f Fix KAT proxy variable declarations 2014-08-05 10:37:32 +02:00
Pieter Janssens 568f8327ef Merge remote-tracking branch 'origin/develop' 2014-08-05 10:13:19 +02:00
Pieter Janssens 7e2b7b7376 Fix syntax error nzbget.py 2014-08-05 10:11:31 +02:00
Pieter Janssens 253e578a98 Merge pull request #1780 from rembo10/develop
Add KAT proxy and fix nzbget URL
2014-08-05 09:59:01 +02:00
Pieter Janssens d590d037ad Fix indenting 2014-08-05 09:53:38 +02:00
Pieter Janssens 64298127c5 Dynamically construct nzbget URL
This allows config for no http prefix or with http/https prefix for the
nzbget URL.
2014-08-05 09:47:05 +02:00
Pieter Janssens 9122136594 Add optional KAT proxy config
Fixes #1778
2014-08-05 09:28:53 +02:00
Ade 1ee76f7488 mb tidy up 2014-08-03 13:14:47 +12:00
Ade 91bfdcd4bd Removed Isohunt, nzbsrus
+ mb fixups
2014-08-03 12:04:46 +12:00
Ade 2b02ae4196 Encoding fix
Fixes https://github.com/rembo10/headphones/issues/1754
2014-07-31 11:31:25 +12:00
Ade 326f866d7e Tweaks 2014-07-27 14:14:10 +12:00
Ade 86b1d6f443 imports not required 2014-07-27 01:16:02 +12:00
Ade 62e35e9a42 utorrent
- Attempt to ensure we have the correct folder for post processing
- Get rutracker working
2014-07-27 01:05:16 +12:00
Ade dc02fb4a06 Rename file fails if starts with '.'
Fixes https://github.com/rembo10/headphones/issues/1317
2014-07-18 20:45:56 +12:00
Ade c639d774cf Album Art Folder Naming fix 2014-07-18 19:35:40 +12:00
Ade 58bc146d11 KAT logging issue 2014-07-18 10:37:36 +12:00
Ade 429e2fcc40 Waffles failing 2014-07-18 08:37:24 +12:00
Ade 7d8140316c Merge branch 'develop' of https://github.com/rembo10/headphones into develop 2014-07-17 19:52:06 +12:00
Ade 3dfb8e0b0b Newznab - require all search term words in title
- Skip Newznab result if not all the search word terms are in the
result title

- removed old newzbin and no longer required usenet retention code
2014-07-17 19:45:09 +12:00
Pieter Janssens 9aeea8bcad Update README.md 2014-07-08 10:25:30 +02:00
Ade 850ce9ac3c Merge branch 'AdeHub-ade' into develop 2014-07-08 19:16:53 +12:00
Ade 61b901a481 Merge branch 'ade' of https://github.com/AdeHub/headphones into AdeHub-ade 2014-07-08 19:15:25 +12:00
Ade 08a2c2d148 Merge branch 'texke-master' into develop 2014-07-08 19:12:28 +12:00
Ade d87bb1f11c Merge branch 'master' of https://github.com/texke/headphones into texke-master 2014-07-08 19:11:54 +12:00
Ade bf4d642125 Merge branch 'basilfx-bugfixes6' into develop 2014-07-08 18:57:26 +12:00
Ade 7163886d31 Merge branch 'bugfixes6' of https://github.com/basilfx/headphones into basilfx-bugfixes6 2014-07-08 18:56:42 +12:00
Ade a3930e1c05 Merge branch 'sandersch-escape_html_entities' into develop 2014-07-08 18:23:17 +12:00
Ade 32a037b1c6 Merge branch 'escape_html_entities' of https://github.com/sandersch/headphones into sandersch-escape_html_entities 2014-07-08 18:15:52 +12:00
Ade c170b2a06d Merge branch 'develop' of https://github.com/rembo10/headphones into develop 2014-07-07 20:26:30 +12:00
Bas Stottelaar 0807c32a1c Check for message length 2014-07-05 14:51:54 +02:00
Bas Stottelaar c447212562 Forgot to remove parenthesis 2014-07-05 14:47:15 +02:00
Pieter Janssens 54bbde5c1d Merge pull request #1736 from piejanssens/patch-1
Update README.md
2014-07-04 21:57:47 +02:00
Pieter Janssens 7009984357 Update README.md
Removed out of date statements. Refer to wiki for installation notes and define rules on posting new issues.
2014-07-04 14:26:48 +02:00
Ade 7229b73862 fix merge conflict 2014-06-28 10:30:56 +12:00
Ade 66b9da36dd Album Search Results Changes
- mb findRelease
get additional info incl Date, Formats, Tracks, Rgid etc, pass to
searchresults
added optional artist to findrelease, if searching by album can now
enter album:artist in the search bar, went for this for now as I didn’t
want to change the design too much but maybe in the future we could
have 2 boxes, artist album and then there’s no need for the dropdown
artist, album

- base
position at search box and persist selected option (uses local storage)
when refreshing, useful if entering multiple albums
increased the search box size a little

- searchresults
album results - new fields from mb, fall back to cover art archive rgid
url if last.fm not found (should get more results), musicbrainz album
icon link, pass mb rgid to addReleaseById to redirect to album page
artist results - musicbrainz artist icon

- importer
addreleaseById - added rgid param to create the album record upfront
with status Loading if from searchresults

- webserve
redirect to album page using rgid from searchresults

- album
spinner while album is loading
2014-06-28 08:18:54 +12:00
Bas Stottelaar 9d9c5f53e7 Code improvements and fix for #1721 2014-06-22 17:50:16 +02:00
Bas Stottelaar 9a7300cda1 Parse response content for HTTP errors when verbose is turned on 2014-06-11 13:20:40 +02:00
Bas Stottelaar e618103f38 Merge remote-tracking branch 'rembo10/develop' into bugfixes6 2014-06-11 12:48:05 +02:00
rembo10 114b6b8a41 Fix for #1691, refresh artist failing for artists added from postprocessor 2014-06-08 09:52:27 -07:00
Bas Stottelaar 5064beffe9 Updated init scripts a little 2014-06-02 10:24:18 +02:00
Bas Stottelaar 19c4e7d7d3 Removed the unnecessary initLogger line 2014-06-02 10:22:37 +02:00
Charlie Sanders 8b799c7904 Use built in CGI module to escape special characters in title on history page 2014-05-26 01:28:05 -05:00
Jens Rogier a6c5894272 Totaltracks wasn't right when switching album release: the 2nd query didn't work (& was redundant?) 2014-05-20 21:39:23 +02:00
Ade 0fdc62c914 searcher filtering changes (please review)
- Filtering of Lossless min/max, Preferred bitrate % and already
downloaded check stops the searcher moving on to torrents if prefer NZB
set (and vice versa). Moved from sort_search_results to new routine

- Preferred bitrate low/high calc not quite right, should be target
size + high %

- Please correct me here but I think this logic ‘elif
headphones.PREFERRED_QUALITY’ is True if Preferred Bitrate
(PREFERRED_QUALITY = 2) which means it will always get lossy and
lossless. Surprised that no one has complained about this. Anyway, what
this means is that the ‘Allow lossless if no good lossy match found’
option kind of works although it shouldn’t! Changed to explicitly state
PREFERRED_QUALITY == 1 and added a flag to allow_lossless which is set
if Preferred Bitrate and High Limit and Allow Lossless are all set

- removed unused bitrate from rutracker
2014-05-17 11:30:18 +12:00
rembo10 88af84579c Merge branch 'develop' 2014-05-15 21:10:41 -07:00
rembo10 917dc7695f Fix for #1627: adding albums didn't add a ReleaseID to the database 2014-05-15 21:07:04 -07:00
rembo10 65e3e4faa2 Merge remote-tracking branch 'BenMagyar/master' into develop 2014-05-15 19:39:34 -07:00
rembo10 9e7ec3a65c Merge remote-tracking branch 'basilfx/bugfixes5' into develop 2014-05-15 19:36:40 -07:00
rembo10 7ac7b127c6 Merge remote-tracking branch 'AdeHub/ade' into develop 2014-05-15 19:35:26 -07:00
rembo10 3945753c5c Merge remote-tracking branch 'texke/master' into develop 2014-05-15 19:30:28 -07:00
rembo10 599585d57a Merge remote-tracking branch 'delphiactual/develop' into develop 2014-05-15 19:29:18 -07:00
BenM 10cf36401f Changed Add Artist and View MB 2014-05-13 21:11:31 -04:00
Bas Stottelaar fe2834f4a6 Be quiet when running daemon 2014-05-12 12:12:40 +02:00
Bas Stottelaar 674617c0f8 Ensure correct types 2014-05-12 12:09:26 +02:00
Bas Stottelaar f92f8928b0 Remove the validator, since it returns a dict on error and a list on results. Validator always expected a dict 2014-05-12 11:48:57 +02:00
Bas Stottelaar 45868bc523 Toggle VERBOSE at runtime. Just surf to /toggleVerbose. Comes in handy for the ones who don't want to change startup scripts 2014-05-12 11:45:28 +02:00
Bas Stottelaar 9b2594355f Let the --quiet option don't silence --verbose if we want verbose 2014-05-12 11:16:58 +02:00
Bas Stottelaar e30cb26200 Clarified meaning of HTTP error codes. Will show if it's local or remote error 2014-05-12 11:11:57 +02:00
Bas Stottelaar 63d3ab1cc1 Advanced option to turn off remote SSL certificate validation 2014-05-12 11:06:02 +02:00
Bas Stottelaar 188d907e10 GET, POST, HEAD etc are in lower case in requests.py 2014-05-12 11:02:25 +02:00
delphiactual e972a86233 Easier to follow logic for uTorrent 2014-05-11 21:26:58 -06:00
Ade 52a76e87ad Allow search provider names in preferred words
Adds to the priority if the word matches the search provider. The
provider priority is weighted based on position in list.
Preferred words in title take precedence over preferred providers.
Would be great if we could do something like sb, so until then…
2014-05-11 19:35:41 +12:00
Ade 8ab19f8121 remove rutracker track count check
should be able to filter out unwanted entries with Quality and Search
Word settings
2014-05-10 19:06:09 +12:00
Ade 6f3c8c68d4 Lossless min/max filter
filter results if calculated target size outside bitrate range
2014-05-10 18:53:58 +12:00
Ade 98385ff4c8 Transmission change for rutracker
use metainfo for local .torrent files (will only be rutracker)
2014-05-10 09:32:05 +12:00
delphiactual 62a6be26af PyNMA lib 2014-05-07 08:19:08 -06:00
delphiactual 5c1169ba9d Removed leftover DownloadStation [beta] changes 2014-05-06 21:38:28 -06:00
rembo10 0b47a5ca3a Merge remote-tracking branch 'delphiactual/develop' into develop 2014-05-06 19:51:20 -07:00
delphiactual 9da75a777b NMA fix 2014-05-06 20:22:13 -06:00
delphiactual 2fc2190b59 Merge pull request #2 from rembo10/develop
Develop
2014-05-06 19:48:09 -06:00
rembo10 e108935430 Fix for spinner not showing up anymore when marking albums as wanted 2014-05-06 17:01:30 -07:00
Jens Rogier bbde74db8d Fix: "Ignored" albums not ignored during scheduled updates. 2014-05-06 20:41:18 +02:00
rembo10 9fce4edf00 Fix for #1598, everything bring updated, even albums older than pause_delta 2014-05-06 11:21:37 -07:00
rembo10 ea3c6c3278 Changed VERBOSE back to 1 2014-05-06 11:06:31 -07:00
rembo10 6a0bd397cd Merge remote-tracking branch 'delphiactual/develop' into develop
Conflicts:
	headphones/utorrent.py
2014-05-06 11:05:54 -07:00
delphiactual 7bbb137fd9 uTorrent changes... 2014-05-06 02:12:17 -06:00
rembo10 e67d32445f Merge branch 'develop' 2014-05-05 23:49:47 -07:00
rembo10 83280f3f99 Fix for #1603, typo in utorrent 2014-05-05 23:43:32 -07:00
delphiactual 9c81fbaa2b Merge pull request #1 from rembo10/develop
Develop
2014-05-05 23:23:49 -06:00
rembo10 1d7415b91d Catch errors if we can't grab release info in post processor (#1602) 2014-05-05 18:56:47 -07:00
rembo10 bc5a042dcc Merge branch 'master' into develop 2014-05-05 17:46:25 -07:00
rembo10 e5af0bdd52 Merge branch 'develop' 2014-05-05 17:44:26 -07:00
rembo10 dc2939a4f9 Merge branch 'artist-page-refresh' into develop 2014-05-05 17:38:30 -07:00
rembo10 f52ec56a0f Couple fixes to get everything looking nice 2014-05-05 17:38:18 -07:00
rembo10 75b38df3d6 Typo 2014-05-05 16:58:31 -07:00
rembo10 0f5aeea345 some fixes 2014-05-05 16:58:31 -07:00
rembo10 d80fc36b29 Move to json so we can get the artist name as well 2014-05-05 16:58:31 -07:00
rembo10 4ca42c9c11 Initial changes to stop artist page from refreshing when artist is loading 2014-05-05 16:58:31 -07:00
rembo10 6c760902d6 Merge branch 'develop' 2014-05-05 16:49:58 -07:00
rembo10 2a0fcc7566 Fix for importer failing on adding new release groups 2014-05-05 16:49:43 -07:00
rembo10 b763ccdd08 Don't update all artist statuses when appending a single dir from the postprocessor. Also, don't change any other status but Skipped->Downloaded 2014-05-05 16:41:55 -07:00
rembo10 e72a0d1141 Merge pull request #1599 from delphiactual/master
Fix version check
2014-05-05 16:05:28 -07:00
rembo10 295e5a1ccf Merge branch 'develop': update Headphones Indexer url, fix for #1598, artist updates not working 2014-05-05 16:01:36 -07:00
rembo10 497fafb555 Don't need to query the db twice 2014-05-05 15:09:58 -07:00
rembo10 585f556006 Fix for #1598: Active artist update no longer updates any existing release groups 2014-05-05 14:26:44 -07:00
delphiactual 7b115126c9 Fix version check 2014-05-03 23:10:57 -06:00
rembo10 1a9165a084 Merge remote-tracking branch 'delphiactual/develop' into develop 2014-05-03 18:41:14 -07:00
rembo10 4a74d54cdc Merge remote-tracking branch 'tocsick/master' into develop 2014-05-03 18:40:11 -07:00
rembo10 a0755d4b1c Headphones indexer changes 2014-05-03 18:38:16 -07:00
delphiactual 084181a9a5 password field 2014-05-02 00:46:20 -06:00
delphiactual 5c381b9410 Added Labeling based off of hash for utorrent and fixed a typo in postprocessor.py 2014-05-01 21:58:05 -06:00
rembo10 0be9500518 Merge branch 'utorrent' into develop 2014-05-01 01:10:46 -07:00
rembo10 9c2da72dcc Bug fixes to get utorrent working 2014-05-01 01:09:26 -07:00
rembo10 3f6145cd07 Took out debug logging, put the torrent checking in the right place 2014-05-01 00:28:50 -07:00
rembo10 e155f19d0d Debug logging 2014-05-01 00:14:10 -07:00
rembo10 0c7ea8a5a0 typo 2014-05-01 00:09:45 -07:00
rembo10 1f3474a154 bug fix 2014-05-01 00:06:25 -07:00
rembo10 c0c1f9cc61 Some changes. Getting close 2014-05-01 00:02:29 -07:00
rembo10 e4382c23a3 fixin some bugs 2014-04-30 22:38:20 -07:00
rembo10 29b5305ab4 Slowly making progress 2014-04-30 21:58:46 -07:00
rembo10 758154e87e Some initial changes for utorrent 2014-04-30 21:00:30 -07:00
Kane Wallmann ea8b1815f6 Fixed tab indent to match file 2014-04-30 20:18:48 +10:00
Kane Wallmann ebc0b7205f Added MPC notifier, calls 'mpc update' on local machine 2014-04-30 19:53:59 +10:00
rembo10 06ee770184 Bug fix for postprocessor fix 2014-04-28 20:08:22 -07:00
rembo10 900325186b Url fix for pirate bay (credit to dsm1212) 2014-04-28 20:03:02 -07:00
rembo10 3b9816f3b3 Took out logging messages. Took out nzb possibilities from checkFolder since we're doing it in searcher.py anyways 2014-04-28 19:52:25 -07:00
rembo10 3e5779e5ce more log messages for testing #1575 issue 2014-04-28 19:22:28 -07:00
rembo10 66d0cb4a1b Added some more logging to see why postprocessor is not automatically running 2014-04-28 15:56:52 -07:00
rembo10 54c85c7951 Added some logging to see why postprocessor is not automatically running 2014-04-28 15:55:42 -07:00
rembo10 941da4be1c Merge remote-tracking branch 'sildur/master' into develop 2014-04-28 15:26:13 -07:00
rembo10 69c94baf18 Merge remote-tracking branch 'texke/master' into develop 2014-04-28 15:25:40 -07:00
Jens Rogier 90016f6eda Cleaned up query some more 2014-04-20 19:23:10 +02:00
Roberto Romero 06b647e30d Fix error: AttributeError: 'module' object has no attribute 'POST' 2014-04-19 01:41:23 -03:00
rembo10 84ebcb84f9 Changed 'newznab' -> 'custom newznab providers' to hopefully get rid of some confusion 2014-04-18 11:04:52 -07:00
Jens Rogier b11610face Fixed major mistake where tracklisting on the index would count the total tracks of the whole database when updating albums of the particular artist. 2014-04-18 10:40:15 +02:00
rembo10 6e90419014 Don't need to get commits behind if current version == latest version (versioncheck.py) 2014-04-17 20:44:28 -07:00
rembo10 0457c3c30e Merge branch 'develop': OSX & Boxcar notifications, bug fixes 2014-04-17 20:37:45 -07:00
rembo10 945cfbf21b Took out some leftover boxcar 1 stuff 2014-04-17 17:34:42 -07:00
rembo10 5769cb0dee Merge remote-tracking branch 'NovaXeros/patch-1' into develop
Conflicts:
	headphones/notifiers.py
2014-04-17 17:17:25 -07:00
rembo10 efd043e309 Merge remote-tracking branch 'texke/master' into develop 2014-04-17 17:15:35 -07:00
rembo10 fd19563160 Merge remote-tracking branch 'AdeHub/notify' into develop. Replaced boxcar notifications from daktak
Conflicts:
	headphones/__init__.py
	headphones/notifiers.py
	headphones/sab.py
	headphones/webserve.py
2014-04-17 17:14:37 -07:00
rembo10 1a2ed24566 Merge remote-tracking branch 'basilfx/bugfixes4' into develop 2014-04-17 16:57:19 -07:00
rembo10 d2fecc47ec Possible fix for #1560, what.cd choking when preferred bitrate is not one of the what.cd categories 2014-04-17 13:50:27 -07:00
Jens Rogier f9b0a5efb9 Fixed little bug where the trackcount on the index page was screwing up when choosing different release. 2014-04-17 19:41:40 +02:00
Jens Rogier e304e34ac7 Added ability to ignore albums.
Trackcount on homepage now is now excluding ignored albums.
Added "Manage ignored albums" to the Manage function.
2014-04-17 19:23:39 +02:00
Ade ff3826c0c8 More Boxcar2
Added MusicBrainz link to the message
2014-04-17 23:05:30 +12:00
NovaXeros c96b043a19 Update notifiers.py 2014-04-16 21:01:14 +01:00
NovaXeros a1d0f14eb8 Change LMS title in config options.
Squeezebox is an old unused name, and LMS isn't very self-explanitory.
2014-04-16 20:13:12 +01:00
NovaXeros 0234f4dab9 Update notifiers.py 2014-04-16 20:06:19 +01:00
NovaXeros e00411f7ad Cleaned LMS error reporting.
Even upon successful rescan, the code would throw a keyerror back. Not certain why, but LMS sends some really weird unicode JSON back as its result, unlike XBMC, so having response[0]['result'] will always throw back a keyerror.

Removing the key, however, and just asking the return to find 'result' works fine.
2014-04-16 20:04:14 +01:00
Ade 3b2350cd8f Boxcar2, OS X Notifications
- OS X Notifications require an application bundle, allow user to
create a dummy app in a specified location and then use bundle when
notifying
- Boxcar2
- Moved snatched notify logic to searcher so we can pass extra info
incl provider
2014-04-16 21:41:22 +12:00
Bas Stottelaar d490091a9a Bugfixes in helpers.py for MediaFile errors. Pre-detect potential file errors
Conflicts:
	headphones/postprocessor.py
2014-04-15 10:10:10 +02:00
Bas Stottelaar 5be36b517f Delete by ArtistID, not AlbumID 2014-04-15 10:00:33 +02:00
rembo10 dab323e8ff Better fix for album art stretching 2014-04-14 18:56:10 -07:00
rembo10 7e02f86af8 Fixed import for specifying beets config file 2014-04-14 18:51:24 -07:00
rembo10 26ed43fca8 Fix for album art stretching 2014-04-14 18:46:40 -07:00
rembo10 dab4e3d5f8 Updated beets lib to correctly point to config file (thanks to AdeHub) 2014-04-14 17:38:29 -07:00
rembo10 07e8b6b095 Merge remote-tracking branch 'texke/master' into develop 2014-04-14 17:33:33 -07:00
rembo10 52523a5d3c Merge remote-tracking branch 'daktak/master' into develop 2014-04-14 17:28:34 -07:00
rembo10 ded4b7ef6f Merge remote-tracking branch 'basilfx/bugfixes' into develop 2014-04-14 17:27:22 -07:00
Jens Rogier aa9d669a2b Fixed problem where albums with secondary types still got added 2014-04-14 21:28:06 +02:00
daktak 5378884bf8 boxcar notification 2014-04-14 17:43:01 +10:00
Bas Stottelaar 631d947a9a Re-added decoding of strings 2014-04-13 18:25:17 +02:00
rembo10 7280807588 Don't limit extras to 25 2014-04-12 17:44:48 -07:00
Bas Stottelaar ca010ac522 Prevent misleading messages about whitelisting 2014-04-12 20:35:15 +02:00
Bas Stottelaar e2e1ef9bc6 Add clear log option. Improved styling. 2014-04-12 20:27:38 +02:00
Bas Stottelaar a96b793c98 Add color for warnings.
Some distinction between the levels is handy.
2014-04-12 18:59:08 +02:00
Bas Stottelaar f7ba0ff634 All loggers on the same level.
In the issues, a lot of logs do not include DEBUG info, even if started with
verbose turned on. By defaulting all loggers to INFO, but DEBUG if verbose
is 2, I hope this will improve.

This will disable DEBUG for log file, but I assume verbose is turned on in
case of problems, so it will save disk writings.
2014-04-12 18:56:40 +02:00
Bas Stottelaar a0339d9814 Forgot a parenthesis 2014-04-12 18:23:59 +02:00
Bas Stottelaar eb7a1d7117 Updated methods to log messages new style 2014-04-12 18:23:59 +02:00
Bas Stottelaar 65fa0e21fa Do not decode for logger. 2014-04-12 18:23:59 +02:00
Bas Stottelaar 60ea86d499 Correct indent. Use signum is not None instead of type(signum) != type(None) 2014-04-12 18:23:59 +02:00
Bas Stottelaar 0411bb6b44 Importing with statement is for Python 2.5.
Bases on libraries (e.g. beautifulsoup4), I guess the minimal version for Headphones is at least 2.6
2014-04-12 18:23:58 +02:00
Bas Stottelaar d0198b15ee Restructured imports of post processor 2014-04-12 18:23:58 +02:00
rembo10 c4e8678d8f Fix for removeExtras error 2014-04-12 09:17:52 -07:00
rembo10 d6cd9cd7e4 whitespace 2014-04-11 11:30:51 -07:00
rembo10 6c4da76056 Merge branch 'develop' 2014-04-11 10:40:37 -07:00
rembo10 bf918e6e56 Merge branch 'bug-fixes' into develop 2014-04-11 10:40:00 -07:00
rembo10 0616aae684 Fix for magnet links not being opened on windows 2014-04-11 10:36:41 -07:00
rembo10 2d137ddcd6 Fix for newznab not being searched 2014-04-11 10:35:34 -07:00
rembo10 94a2be275b Merge remote-tracking branch 'basilfx/bugfixes3' into develop 2014-04-11 10:19:24 -07:00
Bas Stottelaar aba4bae39e Comparing against remote branch should be master...local_version, instead the other way around 2014-04-11 17:54:30 +02:00
rembo10 541fe5a610 Merge branch 'bug-fixes' into develop 2014-04-10 17:35:57 -07:00
rembo10 f7df469cc3 Merge branch 'bug-fixes' 2014-04-10 17:35:44 -07:00
rembo10 648fe27604 omgwtfnzbs fix 2014-04-10 17:35:30 -07:00
rembo10 a6532d149c Merge branch 'develop' 2014-04-10 09:56:11 -07:00
rembo10 966c097f76 Merge remote-tracking branch 'basilfx/bugfixes2' into develop 2014-04-10 09:53:50 -07:00
Bas Stottelaar d3d5b9dda2 Fix for transmission authorization problems 2014-04-10 14:12:03 +02:00
Bas Stottelaar a693777433 Add ordereddict (https://pypi.python.org/pypi/ordereddict) for Beets/Python 2.6 compatibility 2014-04-10 13:21:29 +02:00
rembo10 b406c3cb68 Return latest version from github rather than current version 2014-04-09 21:20:29 -07:00
rembo10 432354dcd1 Merge branch 'develop' 2014-04-09 21:12:36 -07:00
rembo10 496e9a0fd5 Fix for XBMC notification 2014-04-09 21:10:42 -07:00
rembo10 d3727d1fef Couple of fixes for the updater 2014-04-09 19:42:28 -07:00
rembo10 a37ce75038 Bug fix to return latest version available rather than None 2014-04-09 19:39:09 -07:00
rembo10 69d1807e9a Possible fix for updater 2014-04-09 19:33:07 -07:00
rembo10 0c32fbe706 'Nother bug in the updater 2014-04-09 18:56:33 -07:00
rembo10 a1ed6901d4 Possible fix for zip not updating 2014-04-09 18:52:31 -07:00
rembo10 1f4ca0ac48 Added yaml for windows 2014-04-09 18:32:08 -07:00
235 changed files with 16598 additions and 22634 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto
+6 -2
View File
@@ -28,8 +28,12 @@ ehthumbs.db
Icon? Icon?
Thumbs.db Thumbs.db
#ignore thumbnails created by windows #Ignore files generated by PyCharm
Thumbs.db .idea/*
#Ignore files generated by vi
*.swp
#Ignore files build by Visual Studio #Ignore files build by Visual Studio
*.obj *.obj
*.exe *.exe
+45 -44
View File
@@ -19,26 +19,26 @@ import os, sys
# Ensure lib added to path, before any other imports # Ensure lib added to path, before any other imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
from headphones import webstart, logger
from configobj import ConfigObj
import locale import locale
import time import time
import signal import signal
import argparse
from lib.configobj import ConfigObj
import headphones import headphones
from headphones import webstart, logger # Register signals, such as CTRL + C
try:
import argparse
except ImportError:
import lib.argparse as argparse
signal.signal(signal.SIGINT, headphones.sig_handler) signal.signal(signal.SIGINT, headphones.sig_handler)
signal.signal(signal.SIGTERM, headphones.sig_handler) signal.signal(signal.SIGTERM, headphones.sig_handler)
def main(): def main():
"""
Headphones application entry point. Parses arguments, setups encoding and
initializes the application.
"""
# Fixed paths to Headphones # Fixed paths to Headphones
if hasattr(sys, 'frozen'): if hasattr(sys, 'frozen'):
headphones.FULL_PATH = os.path.abspath(sys.executable) headphones.FULL_PATH = os.path.abspath(sys.executable)
@@ -63,7 +63,7 @@ def main():
headphones.SYS_ENCODING = 'UTF-8' headphones.SYS_ENCODING = 'UTF-8'
# Set up and gather command line arguments # Set up and gather command line arguments
parser = argparse.ArgumentParser(description='Music add-on for SABnzbd+') parser = argparse.ArgumentParser(description='Music add-on for SABnzbd+, Transmission and more.')
parser.add_argument('-v', '--verbose', action='store_true', help='Increase console logging verbosity') parser.add_argument('-v', '--verbose', action='store_true', help='Increase console logging verbosity')
parser.add_argument('-q', '--quiet', action='store_true', help='Turn off console logging') parser.add_argument('-q', '--quiet', action='store_true', help='Turn off console logging')
@@ -77,16 +77,16 @@ def main():
args = parser.parse_args() args = parser.parse_args()
if args.verbose: if args.verbose:
headphones.VERBOSE = 2 headphones.VERBOSE = True
elif args.quiet: if args.quiet:
headphones.VERBOSE = 0 headphones.QUIET = True
if args.daemon: if args.daemon:
if sys.platform == 'win32': if sys.platform == 'win32':
print "Daemonize not supported under Windows, starting normally" sys.stderr.write("Daemonizing not supported under Windows, starting normally\n")
else: else:
headphones.DAEMON=True headphones.DAEMON = True
headphones.VERBOSE = False headphones.QUIET = True
if args.pidfile: if args.pidfile:
headphones.PIDFILE = str(args.pidfile) headphones.PIDFILE = str(args.pidfile)
@@ -98,13 +98,16 @@ def main():
# The pidfile is only useful in daemon mode, make sure we can write the file properly # The pidfile is only useful in daemon mode, make sure we can write the file properly
if headphones.DAEMON: if headphones.DAEMON:
headphones.CREATEPID = True headphones.CREATEPID = True
try: try:
file(headphones.PIDFILE, 'w').write("pid\n") with open(headphones.PIDFILE, 'w') as fp:
except IOError, e: fp.write("pid\n")
raise SystemExit("Unable to write PID file: %s [%d]" % (e.strerror, e.errno)) except IOError as e:
raise SystemExit("Unable to write PID file: %s [%d]", e.strerror, e.errno)
else: else:
logger.warn("Not running in daemon mode. PID file creation disabled.") logger.warn("Not running in daemon mode. PID file creation disabled.")
# Determine which data directory and config file to use
if args.datadir: if args.datadir:
headphones.DATA_DIR = args.datadir headphones.DATA_DIR = args.datadir
else: else:
@@ -128,47 +131,45 @@ def main():
# Put the database in the DATA_DIR # Put the database in the DATA_DIR
headphones.DB_FILE = os.path.join(headphones.DATA_DIR, 'headphones.db') headphones.DB_FILE = os.path.join(headphones.DATA_DIR, 'headphones.db')
headphones.CFG = ConfigObj(headphones.CONFIG_FILE, encoding='utf-8') headphones.CFG = ConfigObj(headphones.CONFIG_FILE, encoding='utf-8')
# Read config & start logging # Read config and start logging
headphones.initialize() headphones.initialize()
if headphones.DAEMON: if headphones.DAEMON:
if sys.platform == "win32": headphones.daemonize()
print "Daemonize not supported under Windows, starting normally"
else:
headphones.daemonize()
#configure the connection to the musicbrainz database # Configure the connection to the musicbrainz database
headphones.mb.startmb() headphones.mb.startmb()
# Force the http port if neccessary # Force the http port if neccessary
if args.port: if args.port:
http_port = args.port http_port = args.port
logger.info('Using forced port: %i', http_port) logger.info('Using forced web server port: %i', http_port)
else: else:
http_port = int(headphones.HTTP_PORT) http_port = int(headphones.HTTP_PORT)
# Try to start the server. # Try to start the server. Will exit here is address is already in use.
webstart.initialize({ webstart.initialize({
'http_port': http_port, 'http_port': http_port,
'http_host': headphones.HTTP_HOST, 'http_host': headphones.HTTP_HOST,
'http_root': headphones.HTTP_ROOT, 'http_root': headphones.HTTP_ROOT,
'http_proxy': headphones.HTTP_PROXY, 'http_proxy': headphones.HTTP_PROXY,
'enable_https': headphones.ENABLE_HTTPS, 'enable_https': headphones.ENABLE_HTTPS,
'https_cert': headphones.HTTPS_CERT, 'https_cert': headphones.HTTPS_CERT,
'https_key': headphones.HTTPS_KEY, 'https_key': headphones.HTTPS_KEY,
'http_username': headphones.HTTP_USERNAME, 'http_username': headphones.HTTP_USERNAME,
'http_password': headphones.HTTP_PASSWORD, 'http_password': headphones.HTTP_PASSWORD,
}) })
if headphones.LAUNCH_BROWSER and not args.nolaunch: if headphones.LAUNCH_BROWSER and not args.nolaunch:
headphones.launch_browser(headphones.HTTP_HOST, http_port, headphones.HTTP_ROOT) headphones.launch_browser(headphones.HTTP_HOST, http_port,
headphones.HTTP_ROOT)
# Start the background threads # Start the background threads
headphones.start() headphones.start()
# Wait endlessy for a signal to happen
while True: while True:
if not headphones.SIGNAL: if not headphones.SIGNAL:
try: try:
@@ -177,6 +178,7 @@ def main():
headphones.SIGNAL = 'shutdown' headphones.SIGNAL = 'shutdown'
else: else:
logger.info('Received signal: %s', headphones.SIGNAL) logger.info('Received signal: %s', headphones.SIGNAL)
if headphones.SIGNAL == 'shutdown': if headphones.SIGNAL == 'shutdown':
headphones.shutdown() headphones.shutdown()
elif headphones.SIGNAL == 'restart': elif headphones.SIGNAL == 'restart':
@@ -186,7 +188,6 @@ def main():
headphones.SIGNAL = None headphones.SIGNAL = None
return # Call main()
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+65 -44
View File
@@ -1,44 +1,65 @@
#![preview thumb](https://github.com/rembo10/headphones/raw/master/data/images/headphoneslogo.png)Headphones #![preview thumb](https://github.com/rembo10/headphones/raw/master/data/images/headphoneslogo.png)Headphones
###Installation and Notes ###Support & Discuss
This is a relatively early release of a third-party add-on for SABnzbd. It's been around for about a year, and while You are free to join the HP support community on IRC where you can ask questions, hang around and discuss anything related to HP.
quite functional, there are still a lot of kinks to work out!
1. Use any IRC client and connect to the Freenode server.
To run it, just double-click the Headphones.py file (in Windows - you may need to right click and click 'Open With' -> Python) or launch a terminal, cd into the Headphones directory and run 'python headphones.py'. 2. Join #headphones
For additional startup options, type 'python Headphones.py -h' or 'python Headphones.py --help' ###Installation and Notes
###Screenshots [Read our Wiki](../../wiki) on how to install and use HeadPhones properly.
Homepage (Artist Overview) [**Troubleshooting** page](../../wiki/TroubleShooting) in the wiki can help you with comon problems.
![preview thumb](http://i.imgur.com/LZO9a.png) **Issues** can be reported on the GitHub issue tracker considering these rules:
One of the many settings pages.... 1. Analyze your log, you just might find the solution yourself!
2. You read the wiki and searched existing issues, but this is not solving your problem.
![preview thumb](http://i.imgur.com/xcWNy.png) 3. Post the issue with a clear title, description and the HP log and use [proper markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your text (code/log in code blocks).
4. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
It might even know you better than you know yourself:
**Feature requests** can be reported on the GitHub issue tracker too:
![preview thumb](http://i.imgur.com/R7J0f.png)
1. Search for similar existing 'issues', feature requests can be recognized by the label 'Request'.
Import Your Favorite Artists: 2. If a similar Request exists, post a comment (+1, or add a new idea to the existing request), otherwise you can create a new one.
![preview thumb](http://i.imgur.com/6tZoC.png) If you **comply with these rules** you can [post your request/issue](http://github.com/rembo10/headphones/issues).
Artist Search Results (also search by album!): **Support** the project by implementing new features, solving support tickets and provide bug fixes.
If you change something in the code always make a PR to the developer branch instead of the master branch.
![preview thumb](http://i.imgur.com/rIV0P.png)
Artist Page with Bio & Album Overview: ###Screenshots
![preview thumb](http://i.imgur.com/SSil1.png) Homepage (Artist Overview)
Album Page with track overview: ![preview thumb](http://i.imgur.com/LZO9a.png)
![preview thumb](http://i.imgur.com/kcjES.png) One of the many settings pages....
If you run into any issues, use the GitHub issue tracker at: http://github.com/rembo10/headphones/issues. ![preview thumb](http://i.imgur.com/xcWNy.png)
This is free software under the GPL v3 open source license - so feel free to do with it what you wish. It might even know you better than you know yourself:
![preview thumb](http://i.imgur.com/R7J0f.png)
Import Your Favorite Artists:
![preview thumb](http://i.imgur.com/6tZoC.png)
Artist Search Results (also search by album!):
![preview thumb](http://i.imgur.com/rIV0P.png)
Artist Page with Bio & Album Overview:
![preview thumb](http://i.imgur.com/SSil1.png)
Album Page with track overview:
![preview thumb](http://i.imgur.com/kcjES.png)
This is free software under the GPL v3 open source license - so feel free to do with it what you wish.
-310
View File
@@ -1,310 +0,0 @@
.dataTables_wrapper {
border-radius: 20px;
-webkit-border-radius: 20px;
-moz-border-radius: 20px;
width: 88%;
margin: 20px auto 0 auto;
padding: 25px;
background-color: white;
position: relative;
min-height: 155px;
clear: both;
_height: 302px;
zoom: 1; /* Feeling sorry for IE */
}
.dataTables_processing {
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 30px;
margin-left: -125px;
margin-top: -15px;
padding: 14px 0 2px 0;
border: 1px solid #ddd;
text-align: center;
color: #999;
font-size: 14px;
background-color: white;
}
.dataTables_length {
width: 40%;
float: left;
}
.dataTables_filter {
width: 50%;
float: right;
text-align: right;
margin-bottom: 15px;
}
.dataTables_info {
width: 50%;
float: left;
background-color: white;
font-weight: bold;
}
.dataTables_paginate {
width: 44px;
* width: 50px;
float: right;
text-align: right;
background-color: white;
}
/* Pagination nested */
.paginate_disabled_previous, .paginate_enabled_previous, .paginate_disabled_next, .paginate_enabled_next {
height: 19px;
width: 19px;
margin-left: 3px;
float: left;
}
.paginate_disabled_previous {
background-image: url('../images/back_disabled.jpg');
}
.paginate_enabled_previous {
background-image: url('../images/back_enabled.jpg');
}
.paginate_disabled_next {
background-image: url('../images/forward_disabled.jpg');
}
.paginate_enabled_next {
background-image: url('../images/forward_enabled.jpg');
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* DataTables display
*/
table.display {
margin: 20px auto;
clear: both;
/* Note Firefox 3.5 and before have a bug with border-collapse
* ( https://bugzilla.mozilla.org/show%5Fbug.cgi?id=155955 )
* border-spacing: 0; is one possible option. Conditional-css.com is
* useful for this kind of thing
*
* Further note IE 6/7 has problems when calculating widths with border width.
* It subtracts one px relative to the other browsers from the first column, and
* adds one to the end...
*
* If you want that effect I'd suggest setting a border-top/left on th/td's and
* then filling in the gaps with other borders.
*/
}
table.display thead th {
padding: 3px 18px 3px 10px;
background-color: white;
font-weight: bold;
font-size: 16px;
cursor: pointer;
* cursor: hand;
}
table.display tfoot th {
padding: 3px 18px 3px 10px;
border-top: 1px solid black;
font-weight: bold;
}
table.display tr.heading2 td {
border-bottom: 1px solid #aaa;
}
table.display td {
padding: 3px 10px;
font-size: 16px;
}
table.display td.center {
text-align: center;
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* DataTables sorting
*/
.sorting_asc {
background: url('../images/sort_asc.png') no-repeat center right;
}
.sorting_desc {
background: url('../images/sort_desc.png') no-repeat center right;
}
.sorting {
background: url('../images/sort_both.png') no-repeat center right;
}
.sorting_asc_disabled {
background: url('../images/sort_asc_disabled.png') no-repeat center right;
}
.sorting_desc_disabled {
background: url('../images/sort_desc_disabled.png') no-repeat center right;
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* DataTables row classes
*/
table.display tr.odd.gradeA {
background-color: #ddffdd;
}
table.display tr.even.gradeA {
background-color: #ddffdd;
}
table.display tr.odd.gradeC {
background-color: #ddddff;
}
table.display tr.even.gradeC {
background-color: #ddddff;
}
table.display tr.odd.gradeX {
background-color: #ffdddd;
}
table.display tr.even.gradeX {
background-color: #ffdddd;
}
table.display tr.odd.gradeU {
background-color: #ddd;
}
table.display tr.even.gradeU {
background-color: #eee;
}
table.display tr.odd.gradeZ {
background-color: #FCFAFA;
}
table.display tr.even.gradeZ {
background-color: white;
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Misc
*/
.dataTables_scroll {
clear: both;
}
.dataTables_scrollBody {
*margin-top: -1px;
}
.top, .bottom {
padding: 15px;
background-color: #F5F5F5;
border: 1px solid #CCCCCC;
}
.top .dataTables_info {
float: none;
}
.clear {
clear: both;
}
.dataTables_empty {
font-size: 24px;
text-align: center;
vertical-align: middle;
background-color: white;
height: 50px;
width: 90%;
}
tfoot input {
margin: 0.5em 0;
width: 100%;
color: #444;
}
tfoot input.search_init {
color: #999;
}
td.group {
background-color: #d1cfd0;
border-bottom: 2px solid #A19B9E;
border-top: 2px solid #A19B9E;
}
td.details {
background-color: #d1cfd0;
border: 2px solid #A19B9E;
}
.paging_full_numbers {
width: 400px;
height: 22px;
line-height: 22px;
}
.paging_full_numbers span.paginate_button,
.paging_full_numbers span.paginate_active {
border: 1px solid #aaa;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
padding: 2px 5px;
margin: 0 3px;
cursor: pointer;
*cursor: hand;
}
.paging_full_numbers span.paginate_button {
background-color: #ddd;
}
.paging_full_numbers span.paginate_button:hover {
background-color: #ccc;
}
.paging_full_numbers span.paginate_active {
background-color: #99B3FF;
}
table.display tr.even.row_selected td {
background-color: #B0BED9;
}
table.display tr.odd.row_selected td {
background-color: #9FAFD1;
}
div.box {
height: 100px;
padding: 10px;
overflow: auto;
border: 1px solid #8080FF;
background-color: #E5E5FF;
}
Vendored Executable
+19
View File
File diff suppressed because one or more lines are too long
-315
View File
@@ -1,315 +0,0 @@
/* HTML5 ✰ Boilerplate */
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
blockquote, q { quotes: none; }
blockquote:before, blockquote:after,
q:before, q:after { content: ''; content: none; }
ins { background-color: #ff9; color: #000; text-decoration: none; }
mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; }
del { text-decoration: line-through; }
abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; }
table { border-collapse: collapse; border-spacing: 0; }
hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
input, select { vertical-align: middle; }
body { font:13px/1.231 sans-serif; *font-size:small; }
select, input, textarea, button { font:99% sans-serif; }
pre, code, kbd, samp { font-family: monospace, sans-serif; }
html { overflow-y: scroll; }
a:hover, a:active { outline: none; }
ul, ol { margin-left: 2em; }
ol { list-style-type: decimal; }
nav ul, nav li { margin: 0; list-style:none; list-style-image: none; }
small { font-size: 85%; }
strong, th { font-weight: bold; }
td { vertical-align: top; }
sub, sup { font-size: 75%; line-height: 0; position: relative; }
sup { top: -0.5em; }
sub { bottom: -0.25em; }
pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; padding: 15px; }
textarea { overflow: auto; }
.ie6 legend, .ie7 legend { margin-left: -7px; }
input[type="radio"] { vertical-align: text-bottom; }
input[type="checkbox"] { vertical-align: bottom; }
.ie7 input[type="checkbox"] { vertical-align: baseline; }
.ie6 input { vertical-align: text-bottom; }
label, input[type="button"], input[type="submit"], input[type="image"], button { cursor: pointer; }
button, input, select, textarea { margin: 0; }
input:valid, textarea:valid { }
input:invalid, textarea:invalid { border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red; }
.no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd; }
::-moz-selection{ background: grey; color:#fff; text-shadow: none; }
::selection { background: grey; color:#fff; text-shadow: none; }
button { width: auto; overflow: visible; }
.ie7 img { -ms-interpolation-mode: bicubic; }
body, select, input, textarea { color: #444; }
h1, h2, h3, h4, h5, h6 { font-weight: bold; }
/*
// ========================================== \\
|| ||
|| Custom Styles ||
|| ||
\\ ========================================== //
*/
a:link {
color: #5E2612;
text-decoration: none;
}
a:visited {
color: #5E2612;
text-decoration: none;
}
a:hover { /*this effect is not shown in NN4.xx*/
color: #999999;
text-decoration: underline;
}
a:active {/*colour in NN4.xx is red*/
color: #5E2612;
text-decoration: underline;
}
a.blue {
color: blue;
}
container { }
body { background-color: #EBF4FB; min-width: 935px; }
header { min-height: 68px; width: 100%; min-width: 935px; padding-left: 0px; padding-right: 10px; background-color: #CDC9C9; position: fixed; z-index: 998; }
h1 { font-size: 24px; }
h2 { font-size: 20px; }
h3 { font-size: 16px; }
h4 { font-size: 14px; }
p.indented { padding-top: 20px; margin-left: 20px; font-size: 14px; }
p.center { text-align: center; font-size: 18px; }
.smalltext2 { font-size: 12px; margin-left: 45px; }
div#updatebar { text-align: center; min-width: 970px; width: 100%; background-color: light-blue; float: left; }
div#logo { float: left; padding-left: 10px; }
ul#nav { margin: 25px 0 0 0; float: left; list-style-type: none; }
ul#nav li { margin: 40px 0px auto 10px; display: inline; }
ul#nav li a { padding: 5px; font-size: 16px; font-weight: bold; color: #330000; text-decoration: none; }
ul#nav li a:hover { background-color: #a3e532; }
div#subhead_container { height: 30px; width:100%; min-width: 1000px; background-color:#330000; float: left; list-style-type: none; z-index: 998; overflow: hidden; }
ul#subhead_menu { margin-top: 5px; }
ul#subhead_menu li { width: 100%; height: 100%; display: inline; }
ul#subhead_menu li a { padding: 5px 15px 10px 15px; vertical-align: middle; color: white; font-size: 16px; text-decoration: none; }
ul#subhead_menu li a:hover { width: 100%; height: 100%; background-color: #grey; }
div#searchbar { margin: 24px 30px auto auto; float: right; }
div#main { margin: 0; padding: 80px 0 0 0; }
.table_wrapper { border-radius: 20px; -webkit-border-radius: 20px; -moz-border-radius: 20px; width: 88%; margin: 20px auto 0 auto; padding: 25px; background-color: white; position: relative; min-height: 200px; clear: both; _height: 302px; zoom: 1; }
.manage_wrapper { width: 88%; margin: 20px auto 0 auto; padding: 25px; min-height: 150px; clear: both; _height: 302px; zoom: 1; }
.table_wrapper_left { padding: 25px; background-color: #ffffff; float: left; width: 40%; min-height: 100px; margin-top: 25px; margin-left: 30px; margin-right: auto; -moz-border-radius: 20px; border-radius: 20px; }
.table_wrapper_right{ padding: 25px; background-color: #ffffff; width: 40%; min-height: 100px; margin-top: 25px; margin-left: auto; margin-right: 30px; -moz-border-radius: 20px; border-radius: 20px; }
.configtable { font-size: 14px; line-height:18px; }
.configtable td { width: 350px; padding: 10px; }
.configtable td#middle { vertical-align: middle; }
table#artist_table { background-color: white; width: 100%; padding: 20px; }
table#artist_table th#select { text-align: left; }
table#artist_table th#name { text-align: left; min-width: 200px; }
table#artist_table th#status { text-align: left; min-width: 50px; }
table#artist_table th#album { text-align: left; min-width: 300px; }
table#artist_table th#have { text-align: center; }
table#artist_table td#name { vertical-align: middle; text-align: left; min-width:200px; }
table#artist_table td#status { vertical-align: middle; text-align: left; min-width: 50px; }
table#artist_table td#album { vertical-align: middle; text-align: left; min-width: 300px; }
table#artist_table td#have { vertical-align: middle; }
div#paddingheader { padding-top: 48px; font-size: 24px; font-weight: bold; text-align: center; }
div#nopaddingheader { font-size: 24px; font-weight: bold; text-align: center; }
table#album_table { background-color: white; }
table#album_table th#select { vertical-align: middle; text-align: left; min-width: 10px; }
table#album_table th#albumart { text-align: left; min-width: 50px; }
table#album_table th#albumname { text-align: center; min-width: 150px; }
table#album_table th#reldate { width: 175px; text-align: center; min-width: 70px; }
table#album_table th#status { width: 175px; text-align: center; min-width: 80px; }
table#album_table th#type { width: 175px; text-align: center; min-width: 100px; }
table#album_table th#bitrate { text-align: center; min-width: 60px; }
table#album_table th#albumformat { text-align: center; min-width: 60px; }
table#album_table th#wantlossless { width: 80px; text-align: center; min-width: 80px; }
table#album_table td#select { vertical-align: middle; text-align: left; }
table#album_table td#albumart { vertical-align: middle; text-align: left; }
table#album_table td#albumname { vertical-align: middle; text-align: center; }
table#album_table td#reldate { vertical-align: middle; text-align: center; }
table#album_table td#status { vertical-align: middle; text-align: center; font-size: 13px; }
table#album_table td#status a#wantlossless { white-space: nowrap; }
table#album_table td#type { vertical-align: middle; text-align: center; }
table#album_table td#have { vertical-align: middle; }
table#album_table td#bitrate { vertical-align: middle; text-align: center; font-size: 13px; }
table#album_table td#albumformat { vertical-align: middle; text-align: center; font-size: 13px; }
table#album_table td#wantlossless { vertical-align: middle; text-align: center; font-size: 13px; }
img.albumArt { float: left; padding-right: 5px; }
div#albumheader { padding-top: 48px; height: 200px; }
div#track_wrapper { margin-left: -50px; padding-top: 20px; font-size: 16px; width: 100%; }
table#track_table th#number { text-align: right; min-width: 10px; }
table#track_table th#name { text-align: center; min-width: 350px; }
table#track_table th#duration { width: 175px; text-align: center; min-width: 100px; }
table#track_table th#location { text-align: center; width: 250px; }
table#track_table th#bitrate { text-align: center; min-width: 75px; }
table#track_table th#format { text-align: center; min-width: 75px; }
table#track_table td#number { vertical-align: middle; text-align: right; }
table#track_table td#name { vertical-align: middle; text-align: center; font-size: 15px; }
table#track_table td#duration { vertical-align: middle; text-align: center; }
table#track_table td#location { vertical-align: middle; text-align: center; font-size: 11px; }
table#track_table td#bitrate { vertical-align: middle; text-align: center; font-size: 12px; }
table#track_table td#format { vertical-align: middle; text-align: center; font-size: 12px; }
table#history_table { background-color: white; width: 100%; font-size: 13px; }
table#history_table td#dateadded { vertical-align: middle; text-align: center; min-width: 150px; font-size: 14px; }
table#history_table td#filename { vertical-align: middle; text-align: center; min-width: 100px; font-size: 15px; }
table#history_table td#size { vertical-align: middle; text-align: center; min-width: 75px; font-size: 14px; }
table#history_table td#status { vertical-align: middle; text-align: center; font-size: 14px; }
table#history_table td#action { vertical-align: middle; text-align: center; font-size: 14px; }
table#log_table { background-color: white; }
table#log_table th#timestamp { text-align: left; min-width: 150px; }
table#log_table th#level { text-align: left; min-width: 60px; }
table#log_table th#message { text-align: left; min-width: 500px; }
table#upcoming_table th#albumart { text-align: center; min-width: 50px; }
table#upcoming_table th#albumname { text-align: center; min-width: 200px; }
table#upcoming_table th#artistname { text-align: center; min-width: 150px; }
table#upcoming_table th#reldate { text-align: center; min-width: 100px; }
table#upcoming_table th#type { text-align: center; min-width: 75px; }
table#upcoming_table td#select { vertical-align: middle; text-align: center; }
table#upcoming_table td#albumart { vertical-align: middle; text-align: center; min-width: 50px; }
table#upcoming_table td#albumname { vertical-align: middle; text-align: center; min-width: 200px; }
table#upcoming_table td#artistname { vertical-align: middle; text-align: center; min-width: 150px; }
table#upcoming_table td#reldate { vertical-align: middle; text-align: center; min-width: 100px; }
table#upcoming_table td#type { vertical-align: middle; text-align: center; min-width: 75px; }
table#upcoming_table td#status { vertical-align: middle; text-align: center; }
table#wanted_table th#albumart { text-align: center; min-width: 50px; }
table#wanted_table th#albumname { text-align: center; min-width: 200px; }
table#wanted_table th#artistname { text-align: center; min-width: 150px; }
table#wanted_table th#reldate { text-align: center; min-width: 100px; }
table#wanted_table th#type { text-align: center; min-width: 75px; }
table#wanted_table td#select { vertical-align: middle; text-align: center; }
table#wanted_table td#albumart { vertical-align: middle; text-align: center; min-width: 50px; }
table#wanted_table td#albumname { vertical-align: middle; text-align: center; min-width: 200px; }
table#wanted_table td#artistname { vertical-align: middle; text-align: center; min-width: 150px; }
table#wanted_table td#reldate { vertical-align: middle; text-align: center; min-width: 100px; }
table#wanted_table td#type { vertical-align: middle; text-align: center; min-width: 75px; }
table#wanted_table td#status { vertical-align: middle; text-align: center; }
table#searchresults_table th#albumname { text-align: left; min-width: 225px; }
table#searchresults_table th#artistname { text-align: center; min-width: 325px; }
table#searchresults_table th#score { text-align: center; min-width: 75px; }
table#searchresults_table td#albumname { vertical-align: middle; text-align: left; min-width: 200px; }
table#searchresults_table td#artistname { vertical-align: middle; text-align: left; min-width: 300px; }
table#searchresults_table td#score { vertical-align: middle; text-align: center; min-width: 75px; }
div.progress-container { border: 1px solid #ccc; width: 100px; height: 14px; margin: 2px 5px 2px 0; padding: 1px; float: left; background: white; }
div.progress-container > div { background-color: #a3e532; height: 14px; }
.havetracks { font-size: 13px; margin-left: 36px; padding-bottom: 3px; vertical-align: middle; }
footer { margin: 20px auto 20px auto; }
div#version { text-align: center; font-weight: bold; }
div#donate { text-align: center; margin: 20px auto 20px auto; }
div#shutdown{ text-align: center; vertical-align: middle; }
.cloudtag { padding-top: 30px; font-size:16px; }
#cloud a.tag1 { font-size: 0.7em; font-weight: 100; }
#cloud a.tag2 { font-size: 0.8em; font-weight: 200; }
#cloud a.tag3 { font-size: 0.9em; font-weight: 300; }
#cloud a.tag4 { font-size: 1.0em; font-weight: 400; }
#cloud a.tag5 { font-size: 1.2em; font-weight: 500; }
#cloud a.tag6 { font-size: 1.4em; font-weight: 600; }
#cloud a.tag7 { font-size: 1.6em; font-weight: 700; }
#cloud a.tag8 { font-size: 1.8em; font-weight: 800; }
#cloud a.tag9 { font-size: 2.2em; font-weight: 900; }
#cloud a.tag10 { font-size: 2.5em; font-weight: 900; }
#cloud { padding: 2px; line-height: 1.5em; text-align: center; }
#cloud a { padding: 0px; }
#cloud { margin: 0; }
#cloud li { display: inline; }
.ir { display: block; text-indent: -999em; overflow: hidden; background-repeat: no-repeat; text-align: left; direction: ltr; }
.hidden { display: none; visibility: hidden; }
.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
.visuallyhidden.focusable:active,
.visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; }
.invisible { visibility: hidden; }
.clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; }
.clearfix:after { clear: both; }
.clearfix { zoom: 1; }
@media all and (orientation:portrait) {
}
@media all and (orientation:landscape) {
}
@media screen and (max-device-width: 480px) {
html { -webkit-text-size-adjust:none; -ms-text-size-adjust:none; }
}
@media print {
* { background: transparent !important; color: black !important; text-shadow: none !important; filter:none !important;
-ms-filter: none !important; }
a, a:visited { color: #444 !important; text-decoration: underline; }
a[href]:after { content: " (" attr(href) ")"; }
abbr[title]:after { content: " (" attr(title) ")"; }
.ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; }
pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
thead { display: table-header-group; }
tr, img { page-break-inside: avoid; }
@page { margin: 0.5cm; }
p, h2, h3 { orphans: 3; widows: 3; }
h2, h3{ page-break-after: avoid; }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

+59 -4
View File
@@ -13,7 +13,7 @@
<div id="subhead_menu"> <div id="subhead_menu">
<a id="menu_link_delete" href="deleteAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Album</a> <a id="menu_link_delete" href="deleteAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Album</a>
%if album['Status'] == 'Skipped': %if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
<a id="menu_link_wanted" href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=False', $(this),true)" data-success="'${album['AlbumTitle']}' added to queue"><i class="fa fa-heart"></i> Mark Album as Wanted</a> <a id="menu_link_wanted" href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=False', $(this),true)" data-success="'${album['AlbumTitle']}' added to queue"><i class="fa fa-heart"></i> Mark Album as Wanted</a>
%elif album['Status'] == 'Wanted': %elif album['Status'] == 'Wanted':
<a id="menu_link_check" href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this));" data-success="Forced checking successful"><i class="fa fa-search"></i> Force Check</a> <a id="menu_link_check" href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this));" data-success="Forced checking successful"><i class="fa fa-search"></i> Force Check</a>
@@ -89,9 +89,15 @@
<div id="albumImg"> <div id="albumImg">
<img height="200" alt="" class="albumArt" src="artwork/album/${album['AlbumID']}"> <img height="200" alt="" class="albumArt" src="artwork/album/${album['AlbumID']}">
</div> </div>
<h1><a href="http://musicbrainz.org/release-group/${album['AlbumID']}">${album['AlbumTitle']}</a></h1> <h1 id="albumname">
<h2><a href="http://musicbrainz.org/artist/${album['ArtistID']}">${album['ArtistName']}</a></h2> <a href="http://musicbrainz.org/release-group/${album['AlbumID']}" id="albumnamelink">${album['AlbumTitle']}</a>
</h1>
<h2 id="artistname">
<a href="http://musicbrainz.org/artist/${album['ArtistID']}" id="artistnamelink">${album['ArtistName']}</a>
</h2>
<% <%
totalduration = myDB.action("SELECT SUM(TrackDuration) FROM tracks WHERE AlbumID=?", [album['AlbumID']]).fetchone()[0] totalduration = myDB.action("SELECT SUM(TrackDuration) FROM tracks WHERE AlbumID=?", [album['AlbumID']]).fetchone()[0]
totaltracks = len(myDB.select("SELECT TrackTitle from tracks WHERE AlbumID=?", [album['AlbumID']])) totaltracks = len(myDB.select("SELECT TrackTitle from tracks WHERE AlbumID=?", [album['AlbumID']]))
@@ -275,9 +281,58 @@
feedback.fadeIn(); feedback.fadeIn();
} }
var loadingMessage = false;
var spinner_active = false;
var loadingtext_active = false;
var refreshInterval;
var wasLoading = false;
var x = 0;
function checkAlbumStatus() {
$.getJSON("getAlbumjson?AlbumID=${album['AlbumID']}", function(data) {
if (data['Status'] == "Loading"){
wasLoading = true;
$('#albumnamelink').text(data["AlbumTitle"]);
$('#artistnamelink').text(data["ArtistName"]);
if (loadingMessage == false){
$("#ajaxMsg").after( "<div id='ajaxMsg2' class='ajaxMsg'></div>" );
showArtistMsg("Getting album information");
loadingMessage = true;
}
if (spinner_active == false){
$('#albumname').prepend('<i class="fa fa-refresh fa-spin" id="albumnamespinner"></i>')
spinner_active = true;
}
if (loadingtext_active == false){
$('#albumname').append('<h3 id="loadingtext"><i>(Album information is currently being loaded)</i></h3>')
loadingtext_active = true;
}
}
else{
if (++x === 10) {
clearInterval(refreshInterval);
}
var sts = $("#artistname").text();
if (wasLoading == true || sts == "Loading"){
location.reload();
$('#albumnamespinner').remove()
$('#loadingtext').remove()
$('#ajaxMsg2').remove()
spinner_active = false
loadingtext_active = false
loadingMessage = false
}
}
});
}
$(document).ready(function() { $(document).ready(function() {
getAlbumInfo(); getAlbumInfo();
initThisPage(); initThisPage();
checkAlbumStatus();
refreshInterval = setInterval(function(){
checkAlbumStatus();
}, 3000);
}); });
</script> </script>
+71 -43
View File
@@ -8,7 +8,7 @@
<%def name="headerIncludes()"> <%def name="headerIncludes()">
<div id="subhead_container"> <div id="subhead_container">
<div id="subhead_menu"> <div id="subhead_menu">
<a id="menu_link_refresh" onclick="doAjaxCall('refreshArtist?ArtistID=${artist['ArtistID']}', $(this)),'table'" href="#" data-success="'${artist['ArtistName']}' is being refreshed"><i class="fa fa-refresh"></i> Refresh Artist</a> <a id="menu_link_refresh" onclick="doSimpleAjaxCall('refreshArtist?ArtistID=${artist['ArtistID']}')" href="#"><i class="fa fa-refresh"></i> Refresh Artist</a>
<a id="menu_link_delete" href="deleteArtist?ArtistID=${artist['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Artist</a> <a id="menu_link_delete" href="deleteArtist?ArtistID=${artist['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Artist</a>
%if artist['Status'] == 'Paused': %if artist['Status'] == 'Paused':
<a id="menu_link_resume" href="#" onclick="doAjaxCall('resumeArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} resumed"><i class="fa fa-play"></i> Resume Artist</a> <a id="menu_link_resume" href="#" onclick="doAjaxCall('resumeArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} resumed"><i class="fa fa-play"></i> Resume Artist</a>
@@ -16,22 +16,22 @@
<a id="menu_link_pauze" href="#" onclick="doAjaxCall('pauseArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} paused"><i class="fa fa-pause"></i> Pause Artist</a> <a id="menu_link_pauze" href="#" onclick="doAjaxCall('pauseArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} paused"><i class="fa fa-pause"></i> Pause Artist</a>
%endif %endif
%if artist['IncludeExtras']: %if artist['IncludeExtras']:
<a id="menu_link_removeextra" href="#" onclick="doAjaxCall('removeExtras?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="Extras removed for ${artist['ArtistName']}"><i class="fa fa-minus"></i> Remove Extras</a> <a id="menu_link_removeextra" href="#" onclick="doAjaxCall('removeExtras?ArtistID=${artist['ArtistID']}&ArtistName=${artist['ArtistName']}',$(this),'submenu&table')" data-success="Extras removed for ${artist['ArtistName']}"><i class="fa fa-minus"></i> Remove Extras</a>
<a class="menu_link_edit" id="menu_link_modifyextra" href="#"><i class="fa fa-pencil"></i> Modify Extras</a> <a class="menu_link_edit" id="menu_link_modifyextra" href="#"><i class="fa fa-pencil"></i> Modify Extras</a>
%else: %else:
<a id="menu_link_getextra" href="#"><i class="fa fa-plus"></i> Get Extras</a> <a id="menu_link_getextra" href="#"><i class="fa fa-plus"></i> Get Extras</a>
%endif %endif
<div id="dialog" title="Choose Which Extras to Fetch" style="display:none" class="configtable"> <div id="dialog" title="Choose Which Extras to Fetch" style="display:none" class="configtable">
<form action="getExtras" method="get" class="form"> <form action="getExtras" method="get" class="form">
<input type="hidden" name="ArtistID" value="${artist['ArtistID']}"> <input type="hidden" name="ArtistID" value="${artist['ArtistID']}">
<input type="hidden" name="newstyle" value="true"> <input type="hidden" name="newstyle" value="true">
%for extra in extras: %for extra in extras:
<input type="checkbox" id="${extra}" name="${extra}" value="1" ${extras[extra]} />${string.capwords(extra)}<br> <input type="checkbox" id="${extra}" name="${extra}" value="1" ${extras[extra]} />${string.capwords(extra)}<br>
%endfor %endfor
<br> <br>
<input id="submit" type="submit" value="Fetch Extras"> <input id="submit" type="submit" value="Fetch Extras">
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<a href="home" class="back">&laquo; Back to overview</a> <a href="home" class="back">&laquo; Back to overview</a>
@@ -42,14 +42,8 @@
<div id="artistImg"> <div id="artistImg">
<img id="artistImage" class="albumArt" alt="" src="artwork/artist/${artist['ArtistID']}"/> <img id="artistImage" class="albumArt" alt="" src="artwork/artist/${artist['ArtistID']}"/>
</div> </div>
<h1> <h1 id="artistname">
%if artist['Status'] == 'Loading': <a href="http://musicbrainz.org/artist/${artist['ArtistID']}" id="artistnamelink">${artist['ArtistName']}</a>
<i class="fa fa-refresh fa-spin"></i>
%endif
<a href="http://musicbrainz.org/artist/${artist['ArtistID']}">${artist['ArtistName']}</a>
%if artist['Status'] == 'Loading':
<h3><i>(Album information for this artist is currently being loaded)</i></h3>
%endif
</h1> </h1>
<div id="artistBio"></div> <div id="artistBio"></div>
</div> </div>
@@ -62,6 +56,7 @@
<option value="Wanted">Wanted</option> <option value="Wanted">Wanted</option>
<option value="WantedNew">Wanted (new only)</option> <option value="WantedNew">Wanted (new only)</option>
<option value="Skipped">Skipped</option> <option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option> <option value="Downloaded">Downloaded</option>
</select> </select>
<input type="hidden" value="Go"> <input type="hidden" value="Go">
@@ -89,6 +84,8 @@
grade = 'X' grade = 'X'
elif album['Status'] == 'Snatched': elif album['Status'] == 'Snatched':
grade = 'C' grade = 'C'
elif album['Status'] == 'Ignored':
grade = 'I'
else: else:
grade = 'A' grade = 'A'
@@ -123,15 +120,15 @@
%> %>
<tr class="grade${grade}"> <tr class="grade${grade}">
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></td> <td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></td>
<td id="albumart"><img class="albumArt" id="${album['AlbumID']}" src="artwork/thumbs/album/${album['AlbumID']}" height="64" width="64"></td> <td id="albumart"><img class="albumArtnostretch" id="${album['AlbumID']}" src="artwork/thumbs/album/${album['AlbumID']}" height="64" width="64"></td>
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td> <td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
<td id="reldate">${album['ReleaseDate']}</td> <td id="reldate">${album['ReleaseDate']}</td>
<td id="type">${album['Type']}</td> <td id="type">${album['Type']}</td>
<td id="status">${album['Status']} <td id="status">${album['Status']}
%if album['Status'] == 'Skipped': %if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
[<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' added to Wanted list">want</a>] [<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' added to Wanted list">want</a>]
%elif (album['Status'] == 'Wanted' or album['Status'] == 'Wanted Lossless'): %elif (album['Status'] == 'Wanted' or album['Status'] == 'Wanted Lossless'):
[<a href="#" onclick="doAjaxCall('unqueueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' skipped">skip</a>] [<a href="#" onclick="doAjaxCall('unqueueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' skipped">skip</a>] [<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}', $(this),'table')" data-success="Trying to download'${album['AlbumTitle']}'" title="Search if available for download">search</a>]
%else: %else:
[<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}', $(this),'table')" data-success="Retrying the same version of '${album['AlbumTitle']}'" title="Retry the same download again">retry</a>][<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this),'table')" title="Try a new download, skipping all previously tried nzbs" data-success="Downloading new version for '${album['AlbumTitle']}'" data-success="Looking for a new version of '${album['AlbumTitle']}'">new</a>] [<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}', $(this),'table')" data-success="Retrying the same version of '${album['AlbumTitle']}'" title="Retry the same download again">retry</a>][<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this),'table')" title="Try a new download, skipping all previously tried nzbs" data-success="Downloading new version for '${album['AlbumTitle']}'" data-success="Looking for a new version of '${album['AlbumTitle']}'">new</a>]
%endif %endif
@@ -153,9 +150,6 @@
<%def name="headIncludes()"> <%def name="headIncludes()">
<link rel="stylesheet" href="interfaces/default/css/data_table.css"> <link rel="stylesheet" href="interfaces/default/css/data_table.css">
%if artist['Status'] == 'Loading':
<meta http-equiv="refresh" content="5">
%endif
</%def> </%def>
<%def name="javascriptIncludes()"> <%def name="javascriptIncludes()">
@@ -168,13 +162,12 @@
getInfo(elem,id,'artist'); getInfo(elem,id,'artist');
} }
<% <%
if headphones.SONGKICK_FILTER_ENABLED: if headphones.SONGKICK_FILTER_ENABLED:
songkick_filter_enabled = "true" songkick_filter_enabled = "true"
else: else:
songkick_filter_enabled = "false" songkick_filter_enabled = "false"
if not headphones.SONGKICK_LOCATION: if not headphones.SONGKICK_LOCATION:
songkick_location = "none" songkick_location = "none"
else: else:
@@ -197,11 +190,11 @@
function(data){ function(data){
if (data['resultsPage'].totalEntries >= 1) { if (data['resultsPage'].totalEntries >= 1) {
if( ${songkick_filter_enabled} ) { if( ${songkick_filter_enabled} ) {
data.resultsPage.results.event = $.grep(data.resultsPage.results.event, function(element,index){ data.resultsPage.results.event = $.grep(data.resultsPage.results.event, function(element,index){
return element.venue.metroArea.id == ${songkick_location}; return element.venue.metroArea.id == ${songkick_location};
}); });
} }
var tourDate; var tourDate;
@@ -237,18 +230,49 @@
}); });
} }
var loadingMessage = false;
var spinner_active = false;
var loadingtext_active = false;
function checkArtistStatus() {
$.getJSON("getArtistjson?ArtistID=${artist['ArtistID']}", function(data) {
if (data['Status'] == "Loading"){
refreshTable();
$('#artistnamelink').text(data["ArtistName"]);
if (loadingMessage == false){
$("#ajaxMsg").after( "<div id='ajaxMsg2' class='ajaxMsg'></div>" );
showArtistMsg("Getting artist information");
loadingMessage = true;
}
if (spinner_active == false){
$('#artistname').prepend('<i class="fa fa-refresh fa-spin" id="artistnamespinner"></i>')
spinner_active = true;
}
if (loadingtext_active == false){
$('#artistname').append('<h3 id="loadingtext"><i>(Album information for this artist is currently being loaded)</i></h3>')
loadingtext_active = true;
}
}
else{
$('#artistnamespinner').remove()
$('#loadingtext').remove()
$('#ajaxMsg2').remove()
spinner_active = false
loadingtext_active = false
loadingMessage = false
}
});
}
function initThisPage() { function initThisPage() {
$('#menu_link_getextra').click(function() { $('#menu_link_getextra').click(function(event) {
$('#dialog').dialog(); $('#dialog').dialog();
return false; event.preventDefault();
}); });
$('#menu_link_modifyextra').click(function() { $('#menu_link_modifyextra').click(function() {
$('#dialog').dialog(); $('#dialog').dialog();
event.preventDefault();
}); });
%if artist['Status'] == 'Loading':
showMsg("Getting artist information",true);
%endif
$('#album_table').dataTable({ $('#album_table').dataTable({
"bDestroy": true, "bDestroy": true,
"aoColumns": [ "aoColumns": [
@@ -280,7 +304,7 @@
resetFilters("albums"); resetFilters("albums");
setTimeout(function(){ setTimeout(function(){
initFancybox(); initFancybox();
},1500) },1500);
} }
$(document).ready(function() { $(document).ready(function() {
@@ -288,8 +312,12 @@
initThisPage(); initThisPage();
getArtistBio(); getArtistBio();
if( ${songkick_enabled} ){ if( ${songkick_enabled} ){
getArtistsCalendar(); getArtistsCalendar();
} }
checkArtistStatus();
setInterval(function(){
checkArtistStatus();
}, 1500);
}); });
</script> </script>
+38 -13
View File
@@ -3,7 +3,6 @@
from headphones import version from headphones import version
%> %>
<!doctype html> <!doctype html>
<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
<!--[if IE 7 ]> <html lang="en" class="no-js ie7"> <![endif]--> <!--[if IE 7 ]> <html lang="en" class="no-js ie7"> <![endif]-->
<!--[if IE 8 ]> <html lang="en" class="no-js ie8"> <![endif]--> <!--[if IE 8 ]> <html lang="en" class="no-js ie8"> <![endif]-->
<!--[if IE 9 ]> <html lang="en" class="no-js ie9"> <![endif]--> <!--[if IE 9 ]> <html lang="en" class="no-js ie9"> <![endif]-->
@@ -20,16 +19,18 @@
<link rel="shortcut icon" href="images/favicon.ico"> <link rel="shortcut icon" href="images/favicon.ico">
<link rel="apple-touch-icon" href="images/headphoneslogo.png"> <link rel="apple-touch-icon" href="images/headphoneslogo.png">
<link rel="stylesheet" href="css/jquery-ui.min.css">
<link rel="stylesheet" href="interfaces/default/css/style.css"> <link rel="stylesheet" href="interfaces/default/css/style.css">
<link rel="stylesheet" href="interfaces/default/css/jquery-ui.css">
<link rel="stylesheet" href="interfaces/default/css/font-awesome.min.css"> <link rel="stylesheet" href="interfaces/default/css/font-awesome.min.css">
${next.headIncludes()} ${next.headIncludes()}
<script src="js/libs/modernizr-1.7.min.js"></script> <script src="js/libs/modernizr-2.8.3.min.js"></script>
</head> </head>
<body> <body>
<div id="container"> <div id="container">
<div id="ajaxMsg"></div> <div id="ajaxMsg" class="ajaxMsg"></div>
% if not headphones.CURRENT_VERSION: % if not headphones.CURRENT_VERSION:
<div id="updatebar"> <div id="updatebar">
You're running an unknown version of Headphones. <a href="update">Update</a> or You're running an unknown version of Headphones. <a href="update">Update</a> or
@@ -58,11 +59,11 @@
<form action="search" method="get"> <form action="search" method="get">
<input type="text" value="" placeholder="Search" onfocus="if(this.value==this.defaultValue) this.value='';" name="name" /> <input type="text" value="" placeholder="Search" onfocus="if(this.value==this.defaultValue) this.value='';" name="name" />
<i class='fa fa-search mini-icon'></i> <i class='fa fa-search mini-icon'></i>
<select name="type"> <select name="type" id="search_type">
<option value="artist">Artist</option> <option value="artist">Artist</option>
<option value="album">Album</option> <option value="album">Album</option>
</select> </select>
<input type="submit" value="Add"/> <input type="submit" value="Search"/>
</form> </form>
</div> </div>
@@ -77,6 +78,12 @@
</div> </div>
<footer> <footer>
<div id="info">
<small>
<a href="https://github.com/rembo10/headphones"><i class="fa fa-headphones"></i> Website</a> |
<a href="https://github.com/rembo10/headphones/wiki/TroubleShooting"><i class="fa fa-ambulance"></i> Help</a>
</small>
</div>
<div id="actions"> <div id="actions">
<small> <small>
<a href="shutdown"><i class="fa fa-power-off"></i> Shutdown</a> | <a href="shutdown"><i class="fa fa-power-off"></i> Shutdown</a> |
@@ -97,21 +104,39 @@
<a href="#main" id="toTop"><i class="fa fa-angle-double-up"></i> <span>Back to top</span></a> <a href="#main" id="toTop"><i class="fa fa-angle-double-up"></i> <span>Back to top</span></a>
</div> </div>
<script src="js/libs/jquery-1.7.2.min.js"></script> <script src="js/libs/jquery-1.11.1.min.js"></script>
<script src="js/libs/jquery-ui.min.js"></script> <script src="js/libs/jquery-ui.min.js"></script>
<script src="js/common.js"></script>
${next.javascriptIncludes()} ${next.javascriptIncludes()}
<script src="js/plugins.js"></script>
<script src="interfaces/default/js/script.js"></script> <script src="interfaces/default/js/script.js"></script>
<!--[if lt IE 7 ]>
<script src="js/libs/dd_belatedpng.js"></script>
<script> DD_belatedPNG.fix('img, .png_bg');</script>
<![endif]-->
<!-- This template is made by Elmar Kouwenhoven --> <!-- This template is made by Elmar Kouwenhoven -->
<!-- Persist search type using local storage -->
<script type="text/javascript">
$(document).ready(function() {
$('form:first *:input[type!=hidden]:first').focus();
try{
var type = window.localStorage.getItem('search_type');
$("#search_type").val(type);
} catch(e) {
}
});
$('select[id=search_type]').change(function() {
var type = $(this).val()
try{
window.localStorage.setItem('search_type', type);
} catch(e) {
}
});
</script>
</body> </body>
</html> </html>
<%def name="javascriptIncludes()"></%def> <%def name="javascriptIncludes()"></%def>
<%def name="headIncludes()"></%def> <%def name="headIncludes()"></%def>
<%def name="headerIncludes()"></%def> <%def name="headerIncludes()"></%def>
File diff suppressed because it is too large Load Diff
+25 -25
View File
@@ -1,4 +1,4 @@
/* Variables */ /* Variables */
@base-font-face: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; @base-font-face: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
@alt-font-face: "Trebuchet MS", Helvetica, Arial, sans-serif; @alt-font-face: "Trebuchet MS", Helvetica, Arial, sans-serif;
@base-font-size: 12px; @base-font-size: 12px;
@@ -10,20 +10,20 @@
@msg-bg: #FFF6A9; @msg-bg: #FFF6A9;
@msg-bg-success: #D3FFD7; @msg-bg-success: #D3FFD7;
@msg-bg-error: #FFD3D3; @msg-bg-error: #FFD3D3;
/* Mixins */ /* Mixins */
.rounded(@radius: 5px) { .rounded(@radius: 5px) {
-moz-border-radius: @radius; -moz-border-radius: @radius;
-webkit-border-radius: @radius; -webkit-border-radius: @radius;
border-radius: @radius; border-radius: @radius;
} }
.roundedTop(@radius: 5px) { .roundedTop(@radius: 5px) {
-moz-border-radius-topleft: @radius; -moz-border-radius-topleft: @radius;
-moz-border-radius-topright: @radius; -moz-border-radius-topright: @radius;
-webkit-border-top-right-radius: @radius; -webkit-border-top-right-radius: @radius;
-webkit-border-top-left-radius: @radius; -webkit-border-top-left-radius: @radius;
border-top-left-radius: @radius; border-top-left-radius: @radius;
border-top-right-radius: @radius; border-top-right-radius: @radius;
} }
.roundedLeftTop(@radius: 5px) { .roundedLeftTop(@radius: 5px) {
-moz-border-radius-topleft: @radius; -moz-border-radius-topleft: @radius;
@@ -34,14 +34,14 @@
-moz-border-radius-topright: @radius; -moz-border-radius-topright: @radius;
-webkit-border-top-right-radius: @radius; -webkit-border-top-right-radius: @radius;
border-top-right-radius: @radius; border-top-right-radius: @radius;
} }
.roundedBottom(@radius: 5px) { .roundedBottom(@radius: 5px) {
-moz-border-radius-bottomleft: @radius; -moz-border-radius-bottomleft: @radius;
-moz-border-radius-bottomright: @radius; -moz-border-radius-bottomright: @radius;
-webkit-border-bottom-right-radius: @radius; -webkit-border-bottom-right-radius: @radius;
-webkit-border-bottom-left-radius: @radius; -webkit-border-bottom-left-radius: @radius;
border-bottom-left-radius: @radius; border-bottom-left-radius: @radius;
border-bottom-right-radius: @radius; border-bottom-right-radius: @radius;
} }
.roundedLeftBottom(@radius: 5px) { .roundedLeftBottom(@radius: 5px) {
-moz-border-radius-bottomleft: @radius; -moz-border-radius-bottomleft: @radius;
@@ -52,7 +52,7 @@
-moz-border-radius-bottomright: @radius; -moz-border-radius-bottomright: @radius;
-webkit-border-bottom-right-radius: @radius; -webkit-border-bottom-right-radius: @radius;
border-bottom-right-radius: @radius; border-bottom-right-radius: @radius;
} }
.shadow(@shadow: 0 17px 11px -1px #ced8d9) { .shadow(@shadow: 0 17px 11px -1px #ced8d9) {
-moz-box-shadow: @shadow; -moz-box-shadow: @shadow;
-webkit-box-shadow: @shadow; -webkit-box-shadow: @shadow;
@@ -74,4 +74,4 @@
-o-opacity:@opacity_percent / 100 !important; -o-opacity:@opacity_percent / 100 !important;
opacity:@opacity_percent / 100 !important; opacity:@opacity_percent / 100 !important;
} }
@@ -224,6 +224,14 @@ table.display tr.even.gradeA {
background-color: #ddffdd; background-color: #ddffdd;
} }
table.display tr.odd.gradeI {
background-color: #bebebe;
}
table.display tr.even.gradeI {
background-color: #bebebe;
}
table.display tr.odd.gradeC { table.display tr.odd.gradeC {
background-color: #ebf5ff; background-color: #ebf5ff;
} }
@@ -254,6 +262,13 @@ table.display tr.even.gradeU {
background-color: #eee; background-color: #eee;
} }
table.display tr.odd.gradeW {
background-color: #ffffaa;
}
table.display tr.even.gradeW {
background-color: #ffffaa;
}
table.display tr.odd.gradeZ { table.display tr.odd.gradeZ {
background-color: #FAFAFA; background-color: #FAFAFA;
@@ -269,6 +284,7 @@ table.display tr.gradeL #status {
} }
table.display tr.gradeA td, table.display tr.gradeA td,
table.display tr.gradeC td, table.display tr.gradeC td,
table.display tr.gradeI td,
table.display tr.gradeX td, table.display tr.gradeX td,
table.display tr.gradeU td, table.display tr.gradeU td,
table.display tr.gradeZ td {border-bottom: 1px solid #FFF;} table.display tr.gradeZ td {border-bottom: 1px solid #FFF;}
-547
View File
@@ -1,547 +0,0 @@
/*!
* jQuery UI CSS Framework 1.8.19
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Theming/API
*/
/* Layout helpers
----------------------------------*/
.ui-helper-hidden { display: none; }
.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); }
.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; }
.ui-helper-clearfix:after { clear: both; }
.ui-helper-clearfix { zoom: 1; }
.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
/* Interaction Cues
----------------------------------*/
.ui-state-disabled { cursor: default !important; }
/* Icons
----------------------------------*/
/* states and images */
.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
/* Misc visuals
----------------------------------*/
/* Overlays */
.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
/*!
* jQuery UI CSS Framework 1.8.19
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Theming/API
*
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS,%20Helvetica,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=6px&bgColorHeader=dddddd&bgTextureHeader=02_glass.png&bgImgOpacityHeader=35&borderColorHeader=bbbbbb&fcHeader=444444&iconColorHeader=999999&bgColorContent=c9c9c9&bgTextureContent=05_inset_soft.png&bgImgOpacityContent=50&borderColorContent=aaaaaa&fcContent=333333&iconColorContent=999999&bgColorDefault=eeeeee&bgTextureDefault=02_glass.png&bgImgOpacityDefault=60&borderColorDefault=cccccc&fcDefault=3383bb&iconColorDefault=70b2e1&bgColorHover=f8f8f8&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=bbbbbb&fcHover=599fcf&iconColorHover=3383bb&bgColorActive=999999&bgTextureActive=06_inset_hard.png&bgImgOpacityActive=75&borderColorActive=999999&fcActive=ffffff&iconColorActive=454545&bgColorHighlight=eeeeee&bgTextureHighlight=01_flat.png&bgImgOpacityHighlight=55&borderColorHighlight=ffffff&fcHighlight=444444&iconColorHighlight=3383bb&bgColorError=c0402a&bgTextureError=01_flat.png&bgImgOpacityError=55&borderColorError=c0402a&fcError=ffffff&iconColorError=fbc856&bgColorOverlay=eeeeee&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=80&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=60&thicknessShadow=4px&offsetTopShadow=-4px&offsetLeftShadow=-4px&cornerRadiusShadow=0pxdow=0px
*/
/* Component containers
----------------------------------*/
.ui-widget { font-family: Trebuchet MS, Helvetica, Arial, sans-serif; font-size: 1.1em; }
.ui-widget .ui-widget { font-size: 1em; }
.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Trebuchet MS, Helvetica, Arial, sans-serif; font-size: 1em; }
.ui-widget-content { border: 1px solid #aaaaaa; background: #f2f2f2; color: #333333; }
.ui-widget-content a { /*color: #333333;*/ }
.ui-widget-header { border: 1px solid #bbbbbb; background: #dddddd url(../images/ui-bg_glass_35_dddddd_1x400.png) 50% 50% repeat-x; color: #444444; font-weight: bold; }
.ui-widget-header a { color: #444444; }
/* Interaction states
----------------------------------*/
.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #cccccc; background: #eeeeee url(../images/ui-bg_glass_60_eeeeee_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #3383bb; }
.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #3383bb; text-decoration: none; }
.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #bbbbbb; background: #f8f8f8 url(../images/ui-bg_glass_100_f8f8f8_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #599fcf; }
.ui-state-hover a, .ui-state-hover a:hover { color: #599fcf; text-decoration: none; }
.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #999999; background: #ccc; font-weight: bold; color: #ffffff; }
.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #ffffff; text-decoration: none; }
.ui-widget :active { outline: none; }
/* Interaction Cues
----------------------------------*/
.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #ffffff; background: #eeeeee url(../images/ui-bg_flat_55_eeeeee_40x100.png) 50% 50% repeat-x; color: #444444; }
.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #444444; }
.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #c0402a; background: #c0402a url(../images/ui-bg_flat_55_c0402a_40x100.png) 50% 50% repeat-x; color: #ffffff; }
.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; }
.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; }
.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
/* Icons
----------------------------------*/
/* states and images */
.ui-icon { width: 16px; height: 16px; background-image: url(../images/icon_sprite_black.png); }
.ui-widget-content .ui-icon {background-image: url(../images/icon_sprite_black.png); }
.ui-widget-header .ui-icon {background-image: url(../images/icon_sprite_black.png); }
.ui-button.ui-state-hover .ui-icon { background-image: url(../images/icon_sprite_white.png); }
.ui-state-default .ui-icon { background-image: url(../images/ui-icons_70b2e1_256x240.png); }
.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(../images/ui-icons_3383bb_256x240.png); }
.ui-state-active .ui-icon {background-image: url(../images/ui-icons_454545_256x240.png); }
.ui-state-highlight .ui-icon {background-image: url(../images/ui-icons_3383bb_256x240.png); }
.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(../images/ui-icons_fbc856_256x240.png); }
/* positioning */
.ui-icon-carat-1-n { background-position: 0 0; }
.ui-icon-carat-1-ne { background-position: -16px 0; }
.ui-icon-carat-1-e { background-position: -32px 0; }
.ui-icon-carat-1-se { background-position: -48px 0; }
.ui-icon-carat-1-s { background-position: -64px 0; }
.ui-icon-carat-1-sw { background-position: -80px 0; }
.ui-icon-carat-1-w { background-position: -96px 0; }
.ui-icon-carat-1-nw { background-position: -112px 0; }
.ui-icon-carat-2-n-s { background-position: -128px 0; }
.ui-icon-carat-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -64px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -64px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-off { background-position: -96px -144px; }
.ui-icon-radio-on { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-start { background-position: -80px -160px; }
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
/* Misc visuals
----------------------------------*/
/* Corner radius */
.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; -khtml-border-top-left-radius: 6px; border-top-left-radius: 6px; }
.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 6px; -webkit-border-top-right-radius: 6px; -khtml-border-top-right-radius: 6px; border-top-right-radius: 6px; }
.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 6px; -webkit-border-bottom-left-radius: 6px; -khtml-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px; }
.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 6px; -webkit-border-bottom-right-radius: 6px; -khtml-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px; }
/* Overlays */
.ui-widget-overlay { background: #eeeeee url(../images/ui-bg_flat_0_eeeeee_40x100.png) 50% 50% repeat-x; opacity: .80;filter:Alpha(Opacity=80); }
.ui-widget-shadow { margin: -4px 0 0 -4px; padding: 4px; background: #aaaaaa url(../images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .60;filter:Alpha(Opacity=60); -moz-border-radius: 0pxdow=0px; -khtml-border-radius: 0pxdow=0px; -webkit-border-radius: 0pxdow=0px; border-radius: 0pxdow=0px; }/*!
* jQuery UI Resizable 1.8.19
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Resizable#theming
*/
.ui-resizable { position: relative;}
.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block; }
.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/*!
* jQuery UI Selectable 1.8.19
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Selectable#theming
*/
.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; }
/*!
* jQuery UI Accordion 1.8.19
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Accordion#theming
*/
/* IE/Win - Fix animation bug - #4615 */
.ui-accordion { width: 100%; }
.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
.ui-accordion .ui-accordion-li-fix { display: inline; }
.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; }
.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; }
.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; }
.ui-accordion .ui-accordion-content-active { display: block; }
/*!
* jQuery UI Autocomplete 1.8.19
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Autocomplete#theming
*/
.ui-autocomplete { position: absolute; cursor: default; }
/* workarounds */
* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
/*
* jQuery UI Menu @VERSION
*
* Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Menu#theming
*/
.ui-menu {
list-style:none;
padding: 2px;
margin: 0;
display:block;
float: left;
}
.ui-menu .ui-menu {
margin-top: -3px;
}
.ui-menu .ui-menu-item {
margin:0;
padding: 0;
zoom: 1;
float: left;
clear: left;
width: 100%;
}
.ui-menu .ui-menu-item a {
text-decoration:none;
display:block;
padding:.2em .4em;
line-height:1.5;
zoom:1;
}
.ui-menu .ui-menu-item a.ui-state-hover,
.ui-menu .ui-menu-item a.ui-state-active {
font-weight: normal;
margin: -1px;
}
/*!
* jQuery UI Button 1.8.19
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Button#theming
*/
.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
.ui-button:hover {
color:#3383BB;
}
.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
.ui-button-icons-only { width: 3.4em; }
button.ui-button-icons-only { width: 3.7em; }
/*button text element */
.ui-button .ui-button-text { display: block; line-height: 1.4; }
.ui-button-text-only .ui-button-text { padding: .4em 1em; }
.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: 0.2em 0.5em 0.2em 1.8em }
.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: 0.2em 1.8em 0.2em 0.5em }
.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
/* no icon support for input elements, provide padding by default */
input.ui-button { padding: .4em 1em; }
/*button icon element(s) */
.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .3em; }
.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
/*button sets*/
.ui-buttonset { margin-right: 7px; }
.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
/* workarounds */
button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
/*!
* jQuery UI Dialog 1.8.19
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Dialog#theming
*/
.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; }
.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; }
.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
.ui-draggable .ui-dialog-titlebar { cursor: move; }
/*!
* jQuery UI Tabs 1.8.19
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Tabs#theming
*/
.ui-tabs { position: relative; padding: 0em; zoom: 1; background: transparent; border: none;} /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
.ui-tabs .ui-tabs-nav { margin: 0; padding: 0; background: none; border:none;}
.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap;background: #FFF; }
.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; font-weight: normal; }
.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; background-color: #f7f7f7; border-color: #CCC; }
.ui-tabs .ui-tabs-nav li.ui-tabs-selected a { color: #454545; }
.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: #f7f7f7; border: 1px solid #CCC; }
.ui-tabs .ui-tabs-hide { display: none !important; }
/*!
* jQuery UI Datepicker 1.8.19
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Datepicker#theming
*/
.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
.ui-datepicker .ui-datepicker-prev { left:2px; }
.ui-datepicker .ui-datepicker-next { right:2px; }
.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
.ui-datepicker .ui-datepicker-next-hover { right:1px; }
.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; }
.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
.ui-datepicker select.ui-datepicker-month,
.ui-datepicker select.ui-datepicker-year { width: 49%;}
.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; }
.ui-datepicker td { border: 0; padding: 1px; }
.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
/* with multiple calendars */
.ui-datepicker.ui-datepicker-multi { width:auto; }
.ui-datepicker-multi .ui-datepicker-group { float:left; }
.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
/* RTL support */
.ui-datepicker-rtl { direction: rtl; }
.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
.ui-datepicker-rtl .ui-datepicker-group { float:right; }
.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
.ui-datepicker-cover {
display: none; /*sorry for IE5*/
display/**/: block; /*sorry for IE5*/
position: absolute; /*must have*/
z-index: -1; /*must have*/
filter: mask(); /*must have*/
top: -4px; /*must have*/
left: -4px; /*must have*/
width: 200px; /*must have*/
height: 200px; /*must have*/
}/*!
* jQuery UI Progressbar 1.8.19
*
* Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Progressbar#theming
*/
.ui-progressbar { height:2em; text-align: left; overflow: hidden; }
.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }
+76 -10
View File
@@ -163,6 +163,14 @@ img.albumArt {
max-height: 300px; max-height: 300px;
position: relative; position: relative;
} }
img.albumArt-nostretch {
float: left;
min-height: 64px;
min-width: 64px;
max-width: 250px;
max-height: 300px;
position: relative;
}
.title { .title {
margin-bottom: 20px; margin-bottom: 20px;
margin-top: 10px; margin-top: 10px;
@@ -495,7 +503,7 @@ header .wrapper {
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
width: 960px; width: 990px;
} }
header #logo { header #logo {
float: left; float: left;
@@ -537,7 +545,7 @@ footer {
position: relative; position: relative;
top: 4px; top: 4px;
} }
#ajaxMsg { .ajaxMsg {
border: 1px solid #cccccc; border: 1px solid #cccccc;
background-image: -moz-linear-gradient(#ffffff, #eeeeee) !important; background-image: -moz-linear-gradient(#ffffff, #eeeeee) !important;
background-image: linear-gradient(#ffffff, #eeeeee) !important; background-image: linear-gradient(#ffffff, #eeeeee) !important;
@@ -568,16 +576,16 @@ footer {
-o-opacity: 0.8 !important; -o-opacity: 0.8 !important;
opacity: 0.8 !important; opacity: 0.8 !important;
} }
#ajaxMsg .msg { .ajaxMsg .msg {
font-family: "Trebuchet MS", Helvetica, Arial, sans-serif; font-family: "Trebuchet MS", Helvetica, Arial, sans-serif;
line-height: normal; line-height: normal;
padding-left: 20px; padding-left: 20px;
} }
#ajaxMsg .loader { .ajaxMsg .loader {
position: relative; position: relative;
top: 2px; top: 2px;
} }
#ajaxMsg.success { .ajaxMsg .success {
background-image: -moz-linear-gradient(#d3ffd7, #c2edc6) !important; background-image: -moz-linear-gradient(#d3ffd7, #c2edc6) !important;
background-image: linear-gradient(#d3ffd7, #c2edc6) !important; background-image: linear-gradient(#d3ffd7, #c2edc6) !important;
background-image: -webkit-linear-gradient(#d3ffd7, #c2edc6) !important; background-image: -webkit-linear-gradient(#d3ffd7, #c2edc6) !important;
@@ -587,7 +595,7 @@ footer {
padding: 15px 10px; padding: 15px 10px;
text-align: left; text-align: left;
} }
#ajaxMsg.error { .ajaxMsg .error {
background-image: -moz-linear-gradient(#ffd3d3, #edc4c4) !important; background-image: -moz-linear-gradient(#ffd3d3, #edc4c4) !important;
background-image: linear-gradient(#ffd3d3, #edc4c4) !important; background-image: linear-gradient(#ffd3d3, #edc4c4) !important;
background-image: -webkit-linear-gradient(#ffd3d3, #edc4c4) !important; background-image: -webkit-linear-gradient(#ffd3d3, #edc4c4) !important;
@@ -597,7 +605,7 @@ footer {
padding: 15px 10px; padding: 15px 10px;
text-align: left; text-align: left;
} }
#ajaxMsg .ui-icon { .ajaxMsg .ui-icon {
display: inline-block; display: inline-block;
margin-left: -20px; margin-left: -20px;
top: 2px; top: 2px;
@@ -750,7 +758,7 @@ div#searchbar input[type=text] {
line-height: normal; line-height: normal;
margin-right: 5px; margin-right: 5px;
padding: 4px 5px 4px 25px; padding: 4px 5px 4px 25px;
width: 150px; width: 185px;
} }
div#searchbar .mini-icon { div#searchbar .mini-icon {
color: #999; color: #999;
@@ -1118,13 +1126,34 @@ div#artistheader h2 a {
padding: 2px 10px; padding: 2px 10px;
} }
#searchresults_table th#albumname { #searchresults_table th#albumname {
min-width: 225px; min-width: 250px;
text-align: left; text-align: left;
font-size: 14px;
} }
#searchresults_table th#artistname { #searchresults_table th#artistname {
min-width: 325px; min-width: 325px;
text-align: left; text-align: left;
} }
#searchresults_table th#artistnamesmall {
min-width: 125px;
text-align: left;
font-size: 14px;
}
#searchresults_table th#reldate {
min-width: 80px;
text-align: center;
font-size: 14px;
}
#searchresults_table th#format {
min-width: 50px;
text-align: left;
font-size: 14px;
}
#searchresults_table th#tracks {
min-width: 50px;
text-align: left;
font-size: 14px;
}
#searchresults_table #artistImg { #searchresults_table #artistImg {
background: url("../images/loader_black.gif") no-repeat scroll center center #ffffff; background: url("../images/loader_black.gif") no-repeat scroll center center #ffffff;
border: 3px solid #FFFFFF; border: 3px solid #FFFFFF;
@@ -1136,15 +1165,40 @@ div#artistheader h2 a {
width: 50px; width: 50px;
} }
#searchresults_table td#albumname { #searchresults_table td#albumname {
min-width: 200px; min-width: 250px;
text-align: left; text-align: left;
vertical-align: middle; vertical-align: middle;
font-size: 14px;
} }
#searchresults_table td#artistname { #searchresults_table td#artistname {
min-width: 300px; min-width: 300px;
text-align: left; text-align: left;
vertical-align: middle; vertical-align: middle;
} }
#searchresults_table td#artistnamesmall {
min-width: 100px;
text-align: left;
vertical-align: middle;
font-size: 14px;
}
#searchresults_table td#reldate {
min-width: 80px;
text-align: center;
vertical-align: middle;
font-size: 14px;
}
#searchresults_table td#format {
min-width: 50px;
text-align: left;
vertical-align: middle;
font-size: 14px;
}
#searchresults_table td#tracks {
min-width: 50px;
text-align: left;
vertical-align: middle;
font-size: 14px;
}
#searchresults_table td#add { #searchresults_table td#add {
vertical-align: middle; vertical-align: middle;
} }
@@ -1207,6 +1261,13 @@ div#artistheader h2 a {
float: left; float: left;
text-align: left; text-align: left;
} }
#info {
float: left;
text-align: left;
margin-left: 10px;
margin-top: -5px;
color: #cccccc;
}
#actions { #actions {
float: right; float: right;
text-align: right; text-align: right;
@@ -1408,6 +1469,11 @@ div#artistheader h3 span {
min-width: 75px; min-width: 75px;
text-align: center; text-align: center;
} }
#searchresults_table th#scoresmall {
min-width: 50px;
text-align: center;
font-size: 14px;
}
#track_table td#bitrate, #track_table td#bitrate,
#track_table td#format { #track_table td#format {
font-size: 12px; font-size: 12px;
+7
View File
@@ -938,6 +938,13 @@ div#artistheader {
float: left; float: left;
text-align: left; text-align: left;
} }
#info {
float: left;
text-align: left;
margin-left: 10px;
margin-top: -5px;
color: @border-color;
}
#actions { #actions {
float: right; float: right;
text-align: right; text-align: right;
+4 -3
View File
@@ -1,6 +1,7 @@
<%inherit file="base.html"/> <%inherit file="base.html"/>
<%! <%!
from headphones import helpers from headphones import helpers
import cgi
%> %>
<%def name="headerIncludes()"> <%def name="headerIncludes()">
@@ -51,11 +52,11 @@
%> %>
<tr class="grade${grade}"> <tr class="grade${grade}">
<td id="dateadded">${item['DateAdded']}</td> <td id="dateadded">${item['DateAdded']}</td>
<td id="filename">${item['Title']} [<a href="${item['URL']}">${fileid}</a>]<a href="albumPage?AlbumID=${item['AlbumID']}">[album page]</a></td> <td id="filename">${cgi.escape(item['Title'], quote=True)} [<a href="${item['URL']}">${fileid}</a>]<a href="albumPage?AlbumID=${item['AlbumID']}">[album page]</a></td>
<td id="size">${helpers.bytes_to_mb(item['Size'])}</td> <td id="size">${helpers.bytes_to_mb(item['Size'])}</td>
<td id="status">${item['Status']}</td> <td id="status">${item['Status']}</td>
<td id="action">[<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&redirect=history', $(this),'table')" data-success="Retrying download of '${item['Title']}'">retry</a>][<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&new=True&redirect=history',$(this),'table')" data-success="Looking for a new version of '${item['Title']}'">new</a>]</td> <td id="action">[<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&redirect=history', $(this),'table')" data-success="Retrying download of '${cgi.escape(item['Title'], quote=True)}'">retry</a>][<a href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${item['AlbumID']}&new=True&redirect=history',$(this),'table')" data-success="Looking for a new version of '${cgi.escape(item['Title'], quote=True)}'">new</a>]</td>
<td id="delete"><a href="#" onclick="doAjaxCall('clearhistory?date_added=${item['DateAdded']}&title=${item['Title']}',$(this),'table')" data-success="${item['Title']} cleared from history"><img src="interfaces/default/images/trashcan.png" height="18" width="18" id="trashcan" title="Clear this item from the history"></a> <td id="delete"><a href="#" onclick="doAjaxCall('clearhistory?date_added=${item['DateAdded']}&title=${cgi.escape(item['Title'], quote=True)}',$(this),'table')" data-success="${cgi.escape(item['Title'], quote=True)} cleared from history"><img src="interfaces/default/images/trashcan.png" height="18" width="18" id="trashcan" title="Clear this item from the history"></a>
</tr> </tr>
%endfor %endfor
</tbody> </tbody>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

+116 -117
View File
@@ -24,129 +24,128 @@
</%def> </%def>
<%def name="javascriptIncludes()"> <%def name="javascriptIncludes()">
<script src="js/libs/jquery.unveil.min.js"></script>
<script src="js/libs/jquery.dataTables.min.js"></script> <script src="js/libs/jquery.dataTables.min.js"></script>
<script> <script>
function initThisPage() { function initThisPage() {
$('#artist_table').dataTable({ $('#artist_table').dataTable({
"bDestroy": true, "bDestroy": true,
"aoColumnDefs": [ "aoColumnDefs": [
{ {
"bSortable": false, "bSortable": false,
"aTargets": [ 0 ], "aTargets": [0],
"mData":"ArtistID", "mData":"ArtistID",
"mRender": function ( data, type, full ) { "mRender": function ( data, type, full ) {
return '<div id="artistImg"><img class="albumArt" alt="" id="'+ data + '" src="artwork/thumbs/artist/' + data + '"/></div>'; return '<div id="artistImg"><img class="albumArt" alt="" id="'+ data + '" data-src="artwork/thumbs/artist/' + data + '"/></div>';
}
},
{
"aTargets":[1],
"mDataProp":"ArtistSortName",
"mRender":function (data,type,full) {
return '<span title="' + full['ArtistID'] + '"></span><a href="artistPage?ArtistID=' + full['ArtistID'] + '">' + full['ArtistName'] + '</a>'
} }
},
{"aTargets":[2],"mDataProp":"Status"},
{
"aTargets":[3],
"mDataProp":"LatestAlbum",
"mRender":function(data,type,full){
artist = full;
if (artist['ReleaseDate'] && artist['LatestAlbum'])
{
releasedate = artist['ReleaseDate'];
albumdisplay = '<i>' + artist['LatestAlbum'] + '</i> (' + artist['ReleaseDate'] + ')';
}
else if(artist['LatestAlbum'])
{
releasedate = '';
albumdisplay = '<i>' + artist['LatestAlbum'] + '</i>';
}
else
{
releasedate = '';
albumdisplay = '<i>None</i>';
}
if (artist['ReleaseInFuture'] === 'True')
{
grade = 'gradeA';
}
else
{
grade = 'gradeZ';
}
artist['Grade'] = grade;
return '<span title="' + releasedate + '"></span><a href="albumPage?AlbumID=' + full['AlbumID'] + '">' + albumdisplay + '</a>'
}
},
{
"aTargets":[4],
"mDataProp":"HaveTracks",
"mRender":function(data,type,full){
if(full['TotalTracks'] > 0)
{
percent = (full['HaveTracks']*100.0)/full['TotalTracks']
if(percent > 100){
percent = 100;
}
}
else
{
full['TotalTracks'] = '?';
percent = 0;
}
return '<span title="' + percent + '"></span><div class="progress-container"><div style="width:' + percent + '%"><div class="havetracks">' + full['HaveTracks'] + '/' + full['TotalTracks'] + '</div></div></div>';
}},
],
"oLanguage": {
"sSearch": "",
"sLengthMenu":"Show _MENU_ artists per page",
"sInfo":"Showing _START_ to _END_ of _TOTAL_ artists",
"sInfoEmpty":"Showing 0 to 0 of 0 artists",
"sInfoFiltered":"(filtered from _MAX_ total artists)",
"sEmptyTable": " ",
}, },
"bStateSave": true, {
"iDisplayLength": 50, "aTargets":[1],
"sPaginationType": "full_numbers", "mDataProp":"ArtistSortName",
"bProcessing": true, "mRender":function (data,type,full) {
"bServerSide": true, return '<span title="' + full['ArtistID'] + '"></span><a href="artistPage?ArtistID=' + full['ArtistID'] + '">' + full['ArtistName'] + '</a>'
"sAjaxSource": 'getArtists.json', }
"fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ){ },
$('td', nRow).closest('tr').addClass(aData['Grade']) {
nRow.children[0].id = 'albumart'; "aTargets":[2],"mDataProp":"Status"
nRow.children[1].id = 'name'; },
nRow.children[2].id = 'status' {
nRow.children[3].id = 'album' "aTargets":[3],
nRow.children[4].id = 'have' "mDataProp":"LatestAlbum",
return nRow; "mRender":function(data,type,full){
}, artist = full;
"fnServerData": function ( sSource, aoData, fnCallback ) { if (artist['ReleaseDate'] && artist['LatestAlbum'])
/* Add some extra data to the sender */ {
$.getJSON( sSource, aoData, function (json) { fnCallback(json) } ) releasedate = artist['ReleaseDate'];
albumdisplay = '<i>' + artist['LatestAlbum'] + '</i> (' + artist['ReleaseDate'] + ')';
}
else if(artist['LatestAlbum'])
{
releasedate = '';
albumdisplay = '<i>' + artist['LatestAlbum'] + '</i>';
}
else
{
releasedate = '';
albumdisplay = '<i>None</i>';
}
if (artist['ReleaseInFuture'] === 'True')
{
grade = 'gradeA';
}
else
{
grade = 'gradeZ';
}
artist['Grade'] = grade;
return '<span title="' + releasedate + '"></span><a href="albumPage?AlbumID=' + full['AlbumID'] + '">' + albumdisplay + '</a>'
}
},
{
"aTargets":[4],
"mDataProp":"HaveTracks",
"mRender":function(data,type,full){
if(full['TotalTracks'] > 0)
{
percent = (full['HaveTracks']*100.0)/full['TotalTracks']
if(percent > 100){
percent = 100;
}
}
else
{
full['TotalTracks'] = '?';
percent = 0;
}
return '<span title="' + percent + '"></span><div class="progress-container"><div style="width:' + percent + '%"><div class="havetracks">' + full['HaveTracks'] + '/' + full['TotalTracks'] + '</div></div></div>';
}
},
],
"oLanguage": {
"sSearch": "",
"sLengthMenu":"Show _MENU_ artists per page",
"sInfo":"Showing _START_ to _END_ of _TOTAL_ artists",
"sInfoEmpty":"Showing 0 to 0 of 0 artists",
"sInfoFiltered":"(filtered from _MAX_ total artists)",
"sEmptyTable": " ",
}, },
"fnInitComplete": function(oSettings, json) "bStateSave": true,
{ "iDisplayLength": 50,
}}); "sPaginationType": "full_numbers",
"bProcessing": true,
"bServerSide": true,
"sAjaxSource": 'getArtists.json',
"fnRowCallback": function(nRow, aData, iDisplayIndex, iDisplayIndexFull) {
$('td', nRow).closest('tr').addClass(aData['Grade'])
nRow.children[0].id = 'albumart';
nRow.children[1].id = 'name';
nRow.children[2].id = 'status'
nRow.children[3].id = 'album'
nRow.children[4].id = 'have'
return nRow;
},
"fnServerData": function ( sSource, aoData, fnCallback ) {
/* Add some extra data to the sender */
$.getJSON( sSource, aoData, function (json) { fnCallback(json) } )
},
"fnInitComplete": function(oSettings, json)
{
}
});
$('#artist_table').on("draw.dt", function () {
$("img").unveil();
});
resetFilters("artist or album");
}
resetFilters("artist or album"); $(document).ready(function() {
} initThisPage();
});
$(document).ready(function() { $(window).load(function(){
initThisPage(); initFancybox();
}); refreshLoadArtist();
$(window).load(function(){ });
initFancybox();
refreshLoadArtist();
});
</script> </script>
</%def> </%def>
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-648
View File
@@ -1,648 +0,0 @@
/*
* File: jquery.dataTables.min.js
* Version: 1.8.1
* Author: Allan Jardine (www.sprymedia.co.uk)
* Info: www.datatables.net
*
* Copyright 2008-2011 Allan Jardine, all rights reserved.
*
* This source file is free software, under either the GPL v2 license or a
* BSD style license, as supplied with this software.
*
* This source file 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 license files for details.
*/
(function (i, wa, p) {
i.fn.dataTableSettings = [];
var D = i.fn.dataTableSettings; i.fn.dataTableExt = {};
var o = i.fn.dataTableExt;
o.sVersion = "1.8.1";
o.sErrMode = "alert";
o.iApiIndex = 0; o.oApi = {};
o.afnFiltering = [];
o.aoFeatures = [];
o.ofnSearch = {};
o.afnSortData = [];
o.oStdClasses = {
sPagePrevEnabled: "paginate_enabled_previous",
sPagePrevDisabled: "paginate_disabled_previous",
sPageNextEnabled: "paginate_enabled_next",
sPageNextDisabled: "paginate_disabled_next",
sPageJUINext: "", sPageJUIPrev: "",
sPageButton: "paginate_button",
sPageButtonActive: "paginate_active",
sPageButtonStaticDisabled: "paginate_button paginate_button_disabled",
sPageFirst: "first",
sPagePrevious: "previous",
sPageNext: "next",
sPageLast: "last",
sStripOdd: "odd",
sStripEven: "even",
sRowEmpty: "dataTables_empty",
sWrapper: "dataTables_wrapper",
sFilter: "dataTables_filter",
sInfo: "dataTables_info",
sPaging: "dataTables_paginate paging_",
sLength: "dataTables_length",
sProcessing: "dataTables_processing",
sSortAsc: "sorting_asc",
sSortDesc: "sorting_desc",
sSortable: "sorting",
sSortableAsc: "sorting_asc_disabled",
sSortableDesc: "sorting_desc_disabled",
sSortableNone: "sorting_disabled",
sSortColumn: "sorting_",
sSortJUIAsc: "",
sSortJUIDesc: "",
sSortJUI: "",
sSortJUIAscAllowed: "",
sSortJUIDescAllowed: "", sSortJUIWrapper: "", sSortIcon: "", sScrollWrapper: "dataTables_scroll",
sScrollHead: "dataTables_scrollHead",
sScrollHeadInner: "dataTables_scrollHeadInner",
sScrollBody: "dataTables_scrollBody",
sScrollFoot: "dataTables_scrollFoot",
sScrollFootInner: "dataTables_scrollFootInner",
sFooterTH: ""
}; o.oJUIClasses = { sPagePrevEnabled: "fg-button ui-button ui-state-default ui-corner-left",
sPagePrevDisabled: "fg-button ui-button ui-state-default ui-corner-left ui-state-disabled",
sPageNextEnabled: "fg-button ui-button ui-state-default ui-corner-right",
sPageNextDisabled: "fg-button ui-button ui-state-default ui-corner-right ui-state-disabled",
sPageJUINext: "ui-icon ui-icon-circle-arrow-e",
sPageJUIPrev: "ui-icon ui-icon-circle-arrow-w",
sPageButton: "fg-button ui-button ui-state-default",
sPageButtonActive: "fg-button ui-button ui-state-default ui-state-disabled",
sPageButtonStaticDisabled: "fg-button ui-button ui-state-default ui-state-disabled",
sPageFirst: "first ui-corner-tl ui-corner-bl",
sPagePrevious: "previous",
sPageNext: "next",
sPageLast: "last ui-corner-tr ui-corner-br",
sStripOdd: "odd",
sStripEven: "even",
sRowEmpty: "dataTables_empty",
sWrapper: "dataTables_wrapper",
sFilter: "dataTables_filter",
sInfo: "dataTables_info",
sPaging: "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",
sLength: "dataTables_length",
sProcessing: "dataTables_processing",
sSortAsc: "ui-state-default",
sSortDesc: "ui-state-default",
sSortable: "ui-state-default",
sSortableAsc: "ui-state-default",
sSortableDesc: "ui-state-default",
sSortableNone: "ui-state-default",
sSortColumn: "sorting_",
sSortJUIAsc: "css_right ui-icon ui-icon-triangle-1-n",
sSortJUIDesc: "css_right ui-icon ui-icon-triangle-1-s",
sSortJUI: "css_right ui-icon ui-icon-carat-2-n-s",
sSortJUIAscAllowed: "css_right ui-icon ui-icon-carat-1-n",
sSortJUIDescAllowed: "css_right ui-icon ui-icon-carat-1-s",
sSortJUIWrapper: "DataTables_sort_wrapper",
sSortIcon: "DataTables_sort_icon",
sScrollWrapper: "dataTables_scroll",
sScrollHead: "dataTables_scrollHead ui-state-default",
sScrollHeadInner: "dataTables_scrollHeadInner",
sScrollBody: "dataTables_scrollBody",
sScrollFoot: "dataTables_scrollFoot ui-state-default",
sScrollFootInner: "dataTables_scrollFootInner",
sFooterTH: "ui-state-default"
}; o.oPagination = { two_button: { fnInit: function (g, l, r) {
var s, w, y; if (g.bJUI) {
s = p.createElement("a"); w = p.createElement("a"); y = p.createElement("span"); y.className = g.oClasses.sPageJUINext; w.appendChild(y); y = p.createElement("span"); y.className = g.oClasses.sPageJUIPrev;
s.appendChild(y)
}
else { s = p.createElement("div"); w = p.createElement("div") } s.className = g.oClasses.sPagePrevDisabled; w.className = g.oClasses.sPageNextDisabled; s.title = g.oLanguage.oPaginate.sPrevious; w.title = g.oLanguage.oPaginate.sNext; l.appendChild(s); l.appendChild(w); i(s).bind("click.DT", function () { g.oApi._fnPageChange(g, "previous") && r(g) }); i(w).bind("click.DT", function () { g.oApi._fnPageChange(g, "next") && r(g) }); i(s).bind("selectstart.DT", function () { return false }); i(w).bind("selectstart.DT", function () { return false });
if (g.sTableId !== "" && typeof g.aanFeatures.p == "undefined") {
l.setAttribute("id", g.sTableId + "_paginate");
s.setAttribute("id", g.sTableId + "_previous");
w.setAttribute("id", g.sTableId + "_next")
}
}, fnUpdate: function (g) {
if (g.aanFeatures.p) for (var l = g.aanFeatures.p, r = 0, s = l.length; r < s; r++)
if (l[r].childNodes.length !== 0) {
l[r].childNodes[0].className = g._iDisplayStart === 0 ? g.oClasses.sPagePrevDisabled : g.oClasses.sPagePrevEnabled; l[r].childNodes[1].className = g.fnDisplayEnd() == g.fnRecordsDisplay() ? g.oClasses.sPageNextDisabled :
g.oClasses.sPageNextEnabled
}
}
}, iFullNumbersShowPages: 5, full_numbers: { fnInit: function (g, l, r) {
var s = p.createElement("span"), w = p.createElement("span"), y = p.createElement("span"), G = p.createElement("span"), x = p.createElement("span"); s.innerHTML = g.oLanguage.oPaginate.sFirst; w.innerHTML = g.oLanguage.oPaginate.sPrevious; G.innerHTML = g.oLanguage.oPaginate.sNext; x.innerHTML = g.oLanguage.oPaginate.sLast; var v = g.oClasses; s.className = v.sPageButton + " " + v.sPageFirst; w.className = v.sPageButton + " " + v.sPagePrevious; G.className =
v.sPageButton + " " + v.sPageNext; x.className = v.sPageButton + " " + v.sPageLast; l.appendChild(s); l.appendChild(w); l.appendChild(y); l.appendChild(G); l.appendChild(x); i(s).bind("click.DT", function () { g.oApi._fnPageChange(g, "first") && r(g) }); i(w).bind("click.DT", function () { g.oApi._fnPageChange(g, "previous") && r(g) }); i(G).bind("click.DT", function () { g.oApi._fnPageChange(g, "next") && r(g) }); i(x).bind("click.DT", function () { g.oApi._fnPageChange(g, "last") && r(g) }); i("span", l).bind("mousedown.DT", function () { return false }).bind("selectstart.DT",
function () { return false }); if (g.sTableId !== "" && typeof g.aanFeatures.p == "undefined") { l.setAttribute("id", g.sTableId + "_paginate"); s.setAttribute("id", g.sTableId + "_first"); w.setAttribute("id", g.sTableId + "_previous"); G.setAttribute("id", g.sTableId + "_next"); x.setAttribute("id", g.sTableId + "_last") }
}, fnUpdate: function (g, l) {
if (g.aanFeatures.p) {
var r = o.oPagination.iFullNumbersShowPages, s = Math.floor(r / 2), w = Math.ceil(g.fnRecordsDisplay() / g._iDisplayLength), y = Math.ceil(g._iDisplayStart / g._iDisplayLength) + 1, G =
"", x, v = g.oClasses; if (w < r) { s = 1; x = w } else if (y <= s) { s = 1; x = r } else if (y >= w - s) { s = w - r + 1; x = w } else { s = y - Math.ceil(r / 2) + 1; x = s + r - 1 } for (r = s; r <= x; r++) G += y != r ? '<span class="' + v.sPageButton + '">' + r + "</span>" : '<span class="' + v.sPageButtonActive + '">' + r + "</span>"; x = g.aanFeatures.p; var z, Y = function (L) { g._iDisplayStart = (this.innerHTML * 1 - 1) * g._iDisplayLength; l(g); L.preventDefault() }, V = function () { return false }; r = 0; for (s = x.length; r < s; r++) if (x[r].childNodes.length !== 0) {
z = i("span:eq(2)", x[r]); z.html(G); i("span", z).bind("click.DT",
Y).bind("mousedown.DT", V).bind("selectstart.DT", V); z = x[r].getElementsByTagName("span"); z = [z[0], z[1], z[z.length - 2], z[z.length - 1]]; i(z).removeClass(v.sPageButton + " " + v.sPageButtonActive + " " + v.sPageButtonStaticDisabled); if (y == 1) { z[0].className += " " + v.sPageButtonStaticDisabled; z[1].className += " " + v.sPageButtonStaticDisabled } else { z[0].className += " " + v.sPageButton; z[1].className += " " + v.sPageButton } if (w === 0 || y == w || g._iDisplayLength == -1) {
z[2].className += " " + v.sPageButtonStaticDisabled; z[3].className += " " +
v.sPageButtonStaticDisabled
} else { z[2].className += " " + v.sPageButton; z[3].className += " " + v.sPageButton }
}
}
}
}
}; o.oSort = { "string-asc": function (g, l) { if (typeof g != "string") g = ""; if (typeof l != "string") l = ""; g = g.toLowerCase(); l = l.toLowerCase(); return g < l ? -1 : g > l ? 1 : 0 }, "string-desc": function (g, l) { if (typeof g != "string") g = ""; if (typeof l != "string") l = ""; g = g.toLowerCase(); l = l.toLowerCase(); return g < l ? 1 : g > l ? -1 : 0 }, "html-asc": function (g, l) {
g = g.replace(/<.*?>/g, "").toLowerCase(); l = l.replace(/<.*?>/g, "").toLowerCase(); return g <
l ? -1 : g > l ? 1 : 0
}, "html-desc": function (g, l) { g = g.replace(/<.*?>/g, "").toLowerCase(); l = l.replace(/<.*?>/g, "").toLowerCase(); return g < l ? 1 : g > l ? -1 : 0 }, "date-asc": function (g, l) { g = Date.parse(g); l = Date.parse(l); if (isNaN(g) || g === "") g = Date.parse("01/01/1970 00:00:00"); if (isNaN(l) || l === "") l = Date.parse("01/01/1970 00:00:00"); return g - l }, "date-desc": function (g, l) {
g = Date.parse(g); l = Date.parse(l); if (isNaN(g) || g === "") g = Date.parse("01/01/1970 00:00:00"); if (isNaN(l) || l === "") l = Date.parse("01/01/1970 00:00:00"); return l -
g
}, "numeric-asc": function (g, l) { return (g == "-" || g === "" ? 0 : g * 1) - (l == "-" || l === "" ? 0 : l * 1) }, "numeric-desc": function (g, l) { return (l == "-" || l === "" ? 0 : l * 1) - (g == "-" || g === "" ? 0 : g * 1) }
}; o.aTypes = [function (g) { if (typeof g == "number") return "numeric"; else if (typeof g != "string") return null; var l, r = false; l = g.charAt(0); if ("0123456789-".indexOf(l) == -1) return null; for (var s = 1; s < g.length; s++) { l = g.charAt(s); if ("0123456789.".indexOf(l) == -1) return null; if (l == ".") { if (r) return null; r = true } } return "numeric" }, function (g) {
var l = Date.parse(g);
if (l !== null && !isNaN(l) || typeof g == "string" && g.length === 0) return "date"; return null
}, function (g) { if (typeof g == "string" && g.indexOf("<") != -1 && g.indexOf(">") != -1) return "html"; return null } ]; o.fnVersionCheck = function (g) { var l = function (x, v) { for (; x.length < v; ) x += "0"; return x }, r = o.sVersion.split("."); g = g.split("."); for (var s = "", w = "", y = 0, G = g.length; y < G; y++) { s += l(r[y], 3); w += l(g[y], 3) } return parseInt(s, 10) >= parseInt(w, 10) }; o._oExternConfig = { iNextUnique: 0 }; i.fn.dataTable = function (g) {
function l() {
this.fnRecordsTotal =
function () { return this.oFeatures.bServerSide ? parseInt(this._iRecordsTotal, 10) : this.aiDisplayMaster.length }; this.fnRecordsDisplay = function () { return this.oFeatures.bServerSide ? parseInt(this._iRecordsDisplay, 10) : this.aiDisplay.length }; this.fnDisplayEnd = function () { return this.oFeatures.bServerSide ? this.oFeatures.bPaginate === false || this._iDisplayLength == -1 ? this._iDisplayStart + this.aiDisplay.length : Math.min(this._iDisplayStart + this._iDisplayLength, this._iRecordsDisplay) : this._iDisplayEnd }; this.sInstance =
this.oInstance = null; this.oFeatures = { bPaginate: true, bLengthChange: true, bFilter: true, bSort: true, bInfo: true, bAutoWidth: true, bProcessing: false, bSortClasses: true, bStateSave: false, bServerSide: false, bDeferRender: false }; this.oScroll = { sX: "", sXInner: "", sY: "", bCollapse: false, bInfinite: false, iLoadGap: 100, iBarWidth: 0, bAutoCss: true }; this.aanFeatures = [];
this.oLanguage = { sProcessing: "Processing...", sLengthMenu: "Show _MENU_ artists per page", sZeroRecords: "No matching records found", sEmptyTable: "",
sLoadingRecords: "Loading...", sInfo: "Showing _START_ to _END_ of _TOTAL_ artists", sInfoEmpty: "Showing 0 to 0 of 0 artists", sInfoFiltered: "(filtered from _MAX_ total artists)", sInfoPostFix: "", sSearch: "Filter:", sUrl: "", oPaginate: { sFirst: "First", sPrevious: "Previous", sNext: "Next", sLast: "Last" }, fnInfoCallback: null
}; this.aoData = []; this.aiDisplay = []; this.aiDisplayMaster = []; this.aoColumns = []; this.aoHeader = []; this.aoFooter = []; this.iNextId = 0; this.asDataSearch = []; this.oPreviousSearch = { sSearch: "", bRegex: false,
bSmart: true
}; this.aoPreSearchCols = []; this.aaSorting = [[0, "asc", 0]]; this.aaSortingFixed = null; this.asStripClasses = []; this.asDestoryStrips = []; this.sDestroyWidth = 0; this.fnFooterCallback = this.fnHeaderCallback = this.fnRowCallback = null; this.aoDrawCallback = []; this.fnInitComplete = this.fnPreDrawCallback = null; this.sTableId = ""; this.nTableWrapper = this.nTBody = this.nTFoot = this.nTHead = this.nTable = null; this.bInitialised = this.bDeferLoading = false; this.aoOpenRows = []; this.sDom = "lfrtip"; this.sPaginationType = "two_button";
this.iCookieDuration = 7200; this.sCookiePrefix = "SpryMedia_DataTables_"; this.fnCookieCallback = null; this.aoStateSave = []; this.aoStateLoad = []; this.sAjaxSource = this.oLoadedState = null; this.sAjaxDataProp = "aaData"; this.bAjaxDataGet = true; this.jqXHR = null; this.fnServerData = function (a, b, c, d) { d.jqXHR = i.ajax({ url: a, data: b, success: c, dataType: "json", cache: false, error: function (f, e) { e == "parsererror" && alert("DataTables warning: JSON data from server could not be parsed. This is caused by a JSON formatting error.") } }) };
this.fnFormatNumber = function (a) { if (a < 1E3) return a; else { var b = a + ""; a = b.split(""); var c = ""; b = b.length; for (var d = 0; d < b; d++) { if (d % 3 === 0 && d !== 0) c = "," + c; c = a[b - d - 1] + c } } return c }; this.aLengthMenu = [10, 25, 50, 100]; this.bDrawing = this.iDraw = 0; this.iDrawError = -1; this._iDisplayLength = 10; this._iDisplayStart = 0; this._iDisplayEnd = 10; this._iRecordsDisplay = this._iRecordsTotal = 0; this.bJUI = false; this.oClasses = o.oStdClasses; this.bSortCellsTop = this.bSorted = this.bFiltered = false; this.oInit = null
} function r(a) {
return function () {
var b =
[A(this[o.iApiIndex])].concat(Array.prototype.slice.call(arguments)); return o.oApi[a].apply(this, b)
}
} function s(a) {
var b, c, d = a.iInitDisplayStart; if (a.bInitialised === false) setTimeout(function () { s(a) }, 200); else {
xa(a); V(a); L(a, a.aoHeader); a.nTFoot && L(a, a.aoFooter);
K(a, true); a.oFeatures.bAutoWidth && ea(a);
b = 0; for (c = a.aoColumns.length; b < c; b++)
if (a.aoColumns[b].sWidth !== null) a.aoColumns[b].nTh.style.width = u(a.aoColumns[b].sWidth);
if (a.oFeatures.bSort) R(a);
else if (a.oFeatures.bFilter) M(a, a.oPreviousSearch);
else { a.aiDisplay = a.aiDisplayMaster.slice(); E(a); C(a) } if (a.sAjaxSource !== null && !a.oFeatures.bServerSide) a.fnServerData.call(a.oInstance, a.sAjaxSource, [], function (f) { var e = f; if (a.sAjaxDataProp !== "") e = Z(a.sAjaxDataProp)(f); for (b = 0; b < e.length; b++) v(a, e[b]); a.iInitDisplayStart = d; if (a.oFeatures.bSort) R(a); else { a.aiDisplay = a.aiDisplayMaster.slice(); E(a); C(a) } K(a, false); w(a, f) }, a); else if (!a.oFeatures.bServerSide) { K(a, false); w(a) }
}
} function w(a, b) {
a._bInitComplete = true; if (typeof a.fnInitComplete == "function") typeof b !=
"undefined" ? a.fnInitComplete.call(a.oInstance, a, b) : a.fnInitComplete.call(a.oInstance, a)
} function y(a, b, c) {
n(a.oLanguage, b, "sProcessing"); n(a.oLanguage, b, "sLengthMenu"); n(a.oLanguage, b, "sEmptyTable"); n(a.oLanguage, b, "sLoadingRecords"); n(a.oLanguage, b, "sZeroRecords"); n(a.oLanguage, b, "sInfo"); n(a.oLanguage, b, "sInfoEmpty"); n(a.oLanguage, b, "sInfoFiltered"); n(a.oLanguage, b, "sInfoPostFix"); n(a.oLanguage, b, "sSearch"); if (typeof b.oPaginate != "undefined") {
n(a.oLanguage.oPaginate, b.oPaginate, "sFirst"); n(a.oLanguage.oPaginate,
b.oPaginate, "sPrevious"); n(a.oLanguage.oPaginate, b.oPaginate, "sNext"); n(a.oLanguage.oPaginate, b.oPaginate, "sLast")
} typeof b.sEmptyTable == "undefined" && typeof b.sZeroRecords != "undefined" && n(a.oLanguage, b, "sZeroRecords", "sEmptyTable"); typeof b.sLoadingRecords == "undefined" && typeof b.sZeroRecords != "undefined" && n(a.oLanguage, b, "sZeroRecords", "sLoadingRecords"); c && s(a)
} function G(a, b) {
var c = a.aoColumns.length; b = { sType: null, _bAutoType: true, bVisible: true, bSearchable: true, bSortable: true, asSorting: ["asc", "desc"],
sSortingClass: a.oClasses.sSortable, sSortingClassJUI: a.oClasses.sSortJUI, sTitle: b ? b.innerHTML : "", sName: "", sWidth: null, sWidthOrig: null, sClass: null, fnRender: null, bUseRendered: true, iDataSort: c, mDataProp: c, fnGetData: null, fnSetData: null, sSortDataType: "std", sDefaultContent: null, sContentPadding: "", nTh: b ? b : p.createElement("th"), nTf: null
}; a.aoColumns.push(b); if (typeof a.aoPreSearchCols[c] == "undefined" || a.aoPreSearchCols[c] === null) a.aoPreSearchCols[c] = { sSearch: "", bRegex: false, bSmart: true }; else {
if (typeof a.aoPreSearchCols[c].bRegex ==
"undefined") a.aoPreSearchCols[c].bRegex = true; if (typeof a.aoPreSearchCols[c].bSmart == "undefined") a.aoPreSearchCols[c].bSmart = true
} x(a, c, null)
} function x(a, b, c) {
b = a.aoColumns[b]; if (typeof c != "undefined" && c !== null) {
if (typeof c.sType != "undefined") { b.sType = c.sType; b._bAutoType = false } n(b, c, "bVisible"); n(b, c, "bSearchable"); n(b, c, "bSortable"); n(b, c, "sTitle"); n(b, c, "sName"); n(b, c, "sWidth"); n(b, c, "sWidth", "sWidthOrig"); n(b, c, "sClass"); n(b, c, "fnRender"); n(b, c, "bUseRendered"); n(b, c, "iDataSort"); n(b, c, "mDataProp");
n(b, c, "asSorting"); n(b, c, "sSortDataType"); n(b, c, "sDefaultContent"); n(b, c, "sContentPadding")
} b.fnGetData = Z(b.mDataProp); b.fnSetData = ya(b.mDataProp); if (!a.oFeatures.bSort) b.bSortable = false; if (!b.bSortable || i.inArray("asc", b.asSorting) == -1 && i.inArray("desc", b.asSorting) == -1) { b.sSortingClass = a.oClasses.sSortableNone; b.sSortingClassJUI = "" } else if (b.bSortable || i.inArray("asc", b.asSorting) == -1 && i.inArray("desc", b.asSorting) == -1) { b.sSortingClass = a.oClasses.sSortable; b.sSortingClassJUI = a.oClasses.sSortJUI } else if (i.inArray("asc",
b.asSorting) != -1 && i.inArray("desc", b.asSorting) == -1) { b.sSortingClass = a.oClasses.sSortableAsc; b.sSortingClassJUI = a.oClasses.sSortJUIAscAllowed } else if (i.inArray("asc", b.asSorting) == -1 && i.inArray("desc", b.asSorting) != -1) { b.sSortingClass = a.oClasses.sSortableDesc; b.sSortingClassJUI = a.oClasses.sSortJUIDescAllowed }
} function v(a, b) {
var c; c = typeof b.length == "number" ? b.slice() : i.extend(true, {}, b); b = a.aoData.length; var d = { nTr: null, _iId: a.iNextId++, _aData: c, _anHidden: [], _sRowStripe: "" }; a.aoData.push(d); for (var f,
e = 0, h = a.aoColumns.length; e < h; e++) { c = a.aoColumns[e]; typeof c.fnRender == "function" && c.bUseRendered && c.mDataProp !== null && N(a, b, e, c.fnRender({ iDataRow: b, iDataColumn: e, aData: d._aData, oSettings: a })); if (c._bAutoType && c.sType != "string") { f = H(a, b, e, "type"); if (f !== null && f !== "") { f = fa(f); if (c.sType === null) c.sType = f; else if (c.sType != f) c.sType = "string" } } } a.aiDisplayMaster.push(b); a.oFeatures.bDeferRender || z(a, b); return b
} function z(a, b) {
var c = a.aoData[b], d; if (c.nTr === null) {
c.nTr = p.createElement("tr"); typeof c._aData.DT_RowId !=
"undefined" && c.nTr.setAttribute("id", c._aData.DT_RowId); typeof c._aData.DT_RowClass != "undefined" && i(c.nTr).addClass(c._aData.DT_RowClass); for (var f = 0, e = a.aoColumns.length; f < e; f++) {
var h = a.aoColumns[f]; d = p.createElement("td"); d.innerHTML = typeof h.fnRender == "function" && (!h.bUseRendered || h.mDataProp === null) ? h.fnRender({ iDataRow: b, iDataColumn: f, aData: c._aData, oSettings: a }) : H(a, b, f, "display"); if (h.sClass !== null) d.className = h.sClass; if (h.bVisible) { c.nTr.appendChild(d); c._anHidden[f] = null } else c._anHidden[f] =
d
}
}
} function Y(a) {
var b, c, d, f, e, h, j, k, m; if (a.bDeferLoading || a.sAjaxSource === null) { j = a.nTBody.childNodes; b = 0; for (c = j.length; b < c; b++) if (j[b].nodeName.toUpperCase() == "TR") { k = a.aoData.length; a.aoData.push({ nTr: j[b], _iId: a.iNextId++, _aData: [], _anHidden: [], _sRowStripe: "" }); a.aiDisplayMaster.push(k); h = j[b].childNodes; d = e = 0; for (f = h.length; d < f; d++) { m = h[d].nodeName.toUpperCase(); if (m == "TD" || m == "TH") { N(a, k, e, i.trim(h[d].innerHTML)); e++ } } } } j = $(a); h = []; b = 0; for (c = j.length; b < c; b++) {
d = 0; for (f = j[b].childNodes.length; d <
f; d++) { e = j[b].childNodes[d]; m = e.nodeName.toUpperCase(); if (m == "TD" || m == "TH") h.push(e) }
} h.length != j.length * a.aoColumns.length && J(a, 1, "Unexpected number of TD elements. Expected " + j.length * a.aoColumns.length + " and got " + h.length + ". DataTables does not support rowspan / colspan in the table body, and there must be one cell for each row/column combination."); d = 0; for (f = a.aoColumns.length; d < f; d++) {
if (a.aoColumns[d].sTitle === null) a.aoColumns[d].sTitle = a.aoColumns[d].nTh.innerHTML; j = a.aoColumns[d]._bAutoType;
m = typeof a.aoColumns[d].fnRender == "function"; e = a.aoColumns[d].sClass !== null; k = a.aoColumns[d].bVisible; var t, q; if (j || m || e || !k) {
b = 0; for (c = a.aoData.length; b < c; b++) {
t = h[b * f + d]; if (j && a.aoColumns[d].sType != "string") { q = H(a, b, d, "type"); if (q !== "") { q = fa(q); if (a.aoColumns[d].sType === null) a.aoColumns[d].sType = q; else if (a.aoColumns[d].sType != q) a.aoColumns[d].sType = "string" } } if (m) {
q = a.aoColumns[d].fnRender({ iDataRow: b, iDataColumn: d, aData: a.aoData[b]._aData, oSettings: a }); t.innerHTML = q; a.aoColumns[d].bUseRendered &&
N(a, b, d, q)
} if (e) t.className += " " + a.aoColumns[d].sClass; if (k) a.aoData[b]._anHidden[d] = null; else { a.aoData[b]._anHidden[d] = t; t.parentNode.removeChild(t) }
}
}
}
} function V(a) {
var b, c, d; a.nTHead.getElementsByTagName("tr"); if (a.nTHead.getElementsByTagName("th").length !== 0) { b = 0; for (d = a.aoColumns.length; b < d; b++) { c = a.aoColumns[b].nTh; a.aoColumns[b].sClass !== null && i(c).addClass(a.aoColumns[b].sClass); if (a.aoColumns[b].sTitle != c.innerHTML) c.innerHTML = a.aoColumns[b].sTitle } } else {
var f = p.createElement("tr"); b = 0;
for (d = a.aoColumns.length; b < d; b++) { c = a.aoColumns[b].nTh; c.innerHTML = a.aoColumns[b].sTitle; a.aoColumns[b].sClass !== null && i(c).addClass(a.aoColumns[b].sClass); f.appendChild(c) } i(a.nTHead).html("")[0].appendChild(f); W(a.aoHeader, a.nTHead)
} if (a.bJUI) { b = 0; for (d = a.aoColumns.length; b < d; b++) { c = a.aoColumns[b].nTh; f = p.createElement("div"); f.className = a.oClasses.sSortJUIWrapper; i(c).contents().appendTo(f); var e = p.createElement("span"); e.className = a.oClasses.sSortIcon; f.appendChild(e); c.appendChild(f) } } d = function () {
this.onselectstart =
function () { return false }; return false
}; if (a.oFeatures.bSort) for (b = 0; b < a.aoColumns.length; b++) if (a.aoColumns[b].bSortable !== false) { ga(a, a.aoColumns[b].nTh, b); i(a.aoColumns[b].nTh).bind("mousedown.DT", d) } else i(a.aoColumns[b].nTh).addClass(a.oClasses.sSortableNone); a.oClasses.sFooterTH !== "" && i(">tr>th", a.nTFoot).addClass(a.oClasses.sFooterTH); if (a.nTFoot !== null) { c = S(a, null, a.aoFooter); b = 0; for (d = a.aoColumns.length; b < d; b++) if (typeof c[b] != "undefined") a.aoColumns[b].nTf = c[b] }
} function L(a, b, c) {
var d, f,
e, h = [], j = [], k = a.aoColumns.length; if (typeof c == "undefined") c = false; d = 0; for (f = b.length; d < f; d++) { h[d] = b[d].slice(); h[d].nTr = b[d].nTr; for (e = k - 1; e >= 0; e--) !a.aoColumns[e].bVisible && !c && h[d].splice(e, 1); j.push([]) } d = 0; for (f = h.length; d < f; d++) {
if (h[d].nTr) { a = 0; for (e = h[d].nTr.childNodes.length; a < e; a++) h[d].nTr.removeChild(h[d].nTr.childNodes[0]) } e = 0; for (b = h[d].length; e < b; e++) {
k = c = 1; if (typeof j[d][e] == "undefined") {
h[d].nTr.appendChild(h[d][e].cell); for (j[d][e] = 1; typeof h[d + c] != "undefined" && h[d][e].cell == h[d +
c][e].cell; ) { j[d + c][e] = 1; c++ } for (; typeof h[d][e + k] != "undefined" && h[d][e].cell == h[d][e + k].cell; ) { for (a = 0; a < c; a++) j[d + a][e + k] = 1; k++ } h[d][e].cell.setAttribute("rowspan", c); h[d][e].cell.setAttribute("colspan", k)
}
}
}
} function C(a) {
var b, c, d = [], f = 0, e = false; b = a.asStripClasses.length; c = a.aoOpenRows.length; if (!(a.fnPreDrawCallback !== null && a.fnPreDrawCallback.call(a.oInstance, a) === false)) {
a.bDrawing = true; if (typeof a.iInitDisplayStart != "undefined" && a.iInitDisplayStart != -1) {
a._iDisplayStart = a.oFeatures.bServerSide ?
a.iInitDisplayStart : a.iInitDisplayStart >= a.fnRecordsDisplay() ? 0 : a.iInitDisplayStart; a.iInitDisplayStart = -1; E(a)
} if (a.bDeferLoading) { a.bDeferLoading = false; a.iDraw++ } else if (a.oFeatures.bServerSide) { if (!a.bDestroying && !za(a)) return } else a.iDraw++; if (a.aiDisplay.length !== 0) {
var h = a._iDisplayStart, j = a._iDisplayEnd; if (a.oFeatures.bServerSide) { h = 0; j = a.aoData.length } for (h = h; h < j; h++) {
var k = a.aoData[a.aiDisplay[h]]; k.nTr === null && z(a, a.aiDisplay[h]); var m = k.nTr; if (b !== 0) {
var t = a.asStripClasses[f % b]; if (k._sRowStripe !=
t) { i(m).removeClass(k._sRowStripe).addClass(t); k._sRowStripe = t }
} if (typeof a.fnRowCallback == "function") { m = a.fnRowCallback.call(a.oInstance, m, a.aoData[a.aiDisplay[h]]._aData, f, h); if (!m && !e) { J(a, 0, "A node was not returned by fnRowCallback"); e = true } } d.push(m); f++; if (c !== 0) for (k = 0; k < c; k++) m == a.aoOpenRows[k].nParent && d.push(a.aoOpenRows[k].nTr)
}
} else {
d[0] = p.createElement("tr"); if (typeof a.asStripClasses[0] != "undefined") d[0].className = a.asStripClasses[0]; e = a.oLanguage.sZeroRecords.replace("_MAX_", a.fnFormatNumber(a.fnRecordsTotal()));
if (a.iDraw == 1 && a.sAjaxSource !== null && !a.oFeatures.bServerSide) e = a.oLanguage.sLoadingRecords; else if (typeof a.oLanguage.sEmptyTable != "undefined" && a.fnRecordsTotal() === 0) e = a.oLanguage.sEmptyTable; b = p.createElement("td"); b.setAttribute("valign", "top"); b.colSpan = X(a); b.className = a.oClasses.sRowEmpty; b.innerHTML = e; d[f].appendChild(b)
} typeof a.fnHeaderCallback == "function" && a.fnHeaderCallback.call(a.oInstance, i(">tr", a.nTHead)[0], aa(a), a._iDisplayStart, a.fnDisplayEnd(), a.aiDisplay); typeof a.fnFooterCallback ==
"function" && a.fnFooterCallback.call(a.oInstance, i(">tr", a.nTFoot)[0], aa(a), a._iDisplayStart, a.fnDisplayEnd(), a.aiDisplay); f = p.createDocumentFragment(); b = p.createDocumentFragment(); if (a.nTBody) { e = a.nTBody.parentNode; b.appendChild(a.nTBody); if (!a.oScroll.bInfinite || !a._bInitComplete || a.bSorted || a.bFiltered) { c = a.nTBody.childNodes; for (b = c.length - 1; b >= 0; b--) c[b].parentNode.removeChild(c[b]) } b = 0; for (c = d.length; b < c; b++) f.appendChild(d[b]); a.nTBody.appendChild(f); e !== null && e.appendChild(a.nTBody) } for (b = a.aoDrawCallback.length -
1; b >= 0; b--) a.aoDrawCallback[b].fn.call(a.oInstance, a); a.bSorted = false; a.bFiltered = false; a.bDrawing = false; if (a.oFeatures.bServerSide) { K(a, false); typeof a._bInitComplete == "undefined" && w(a) }
}
} function ba(a) { if (a.oFeatures.bSort) R(a, a.oPreviousSearch); else if (a.oFeatures.bFilter) M(a, a.oPreviousSearch); else { E(a); C(a) } } function za(a) {
if (a.bAjaxDataGet) {
K(a, true); var b = a.aoColumns.length, c = [], d, f; a.iDraw++; c.push({ name: "sEcho", value: a.iDraw }); c.push({ name: "iColumns", value: b }); c.push({ name: "sColumns", value: ha(a) });
c.push({ name: "iDisplayStart", value: a._iDisplayStart }); c.push({ name: "iDisplayLength", value: a.oFeatures.bPaginate !== false ? a._iDisplayLength : -1 }); for (f = 0; f < b; f++) { d = a.aoColumns[f].mDataProp; c.push({ name: "mDataProp_" + f, value: typeof d == "function" ? "function" : d }) } if (a.oFeatures.bFilter !== false) {
c.push({ name: "sSearch", value: a.oPreviousSearch.sSearch }); c.push({ name: "bRegex", value: a.oPreviousSearch.bRegex }); for (f = 0; f < b; f++) {
c.push({ name: "sSearch_" + f, value: a.aoPreSearchCols[f].sSearch }); c.push({ name: "bRegex_" +
f, value: a.aoPreSearchCols[f].bRegex
}); c.push({ name: "bSearchable_" + f, value: a.aoColumns[f].bSearchable })
}
} if (a.oFeatures.bSort !== false) {
d = a.aaSortingFixed !== null ? a.aaSortingFixed.length : 0; var e = a.aaSorting.length; c.push({ name: "iSortingCols", value: d + e }); for (f = 0; f < d; f++) { c.push({ name: "iSortCol_" + f, value: a.aaSortingFixed[f][0] }); c.push({ name: "sSortDir_" + f, value: a.aaSortingFixed[f][1] }) } for (f = 0; f < e; f++) { c.push({ name: "iSortCol_" + (f + d), value: a.aaSorting[f][0] }); c.push({ name: "sSortDir_" + (f + d), value: a.aaSorting[f][1] }) } for (f =
0; f < b; f++) c.push({ name: "bSortable_" + f, value: a.aoColumns[f].bSortable })
} a.fnServerData.call(a.oInstance, a.sAjaxSource, c, function (h) { Aa(a, h) }, a); return false
} else return true
} function Aa(a, b) {
if (typeof b.sEcho != "undefined") if (b.sEcho * 1 < a.iDraw) return; else a.iDraw = b.sEcho * 1; if (!a.oScroll.bInfinite || a.oScroll.bInfinite && (a.bSorted || a.bFiltered)) ia(a); a._iRecordsTotal = b.iTotalRecords; a._iRecordsDisplay = b.iTotalDisplayRecords; var c = ha(a); if (c = typeof b.sColumns != "undefined" && c !== "" && b.sColumns != c) var d =
Ba(a, b.sColumns); b = Z(a.sAjaxDataProp)(b); for (var f = 0, e = b.length; f < e; f++) if (c) { for (var h = [], j = 0, k = a.aoColumns.length; j < k; j++) h.push(b[f][d[j]]); v(a, h) } else v(a, b[f]); a.aiDisplay = a.aiDisplayMaster.slice(); a.bAjaxDataGet = false; C(a); a.bAjaxDataGet = true; K(a, false)
} function xa(a) {
var b = p.createElement("div"); a.nTable.parentNode.insertBefore(b, a.nTable); a.nTableWrapper = p.createElement("div"); a.nTableWrapper.className = a.oClasses.sWrapper; a.sTableId !== "" && a.nTableWrapper.setAttribute("id", a.sTableId + "_wrapper");
a.nTableReinsertBefore = a.nTable.nextSibling; for (var c = a.nTableWrapper, d = a.sDom.split(""), f, e, h, j, k, m, t, q = 0; q < d.length; q++) {
e = 0; h = d[q]; if (h == "<") {
j = p.createElement("div"); k = d[q + 1]; if (k == "'" || k == '"') {
m = ""; for (t = 2; d[q + t] != k; ) { m += d[q + t]; t++ } if (m == "H") m = "fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix"; else if (m == "F") m = "fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix"; if (m.indexOf(".") != -1) {
k = m.split("."); j.setAttribute("id", k[0].substr(1,
k[0].length - 1)); j.className = k[1]
} else if (m.charAt(0) == "#") j.setAttribute("id", m.substr(1, m.length - 1)); else j.className = m; q += t
} c.appendChild(j); c = j
} else if (h == ">") c = c.parentNode; else if (h == "l" && a.oFeatures.bPaginate && a.oFeatures.bLengthChange) { f = Ca(a); e = 1 } else if (h == "f" && a.oFeatures.bFilter) { f = Da(a); e = 1 } else if (h == "r" && a.oFeatures.bProcessing) { f = Ea(a); e = 1 } else if (h == "t") { f = Fa(a); e = 1 } else if (h == "i" && a.oFeatures.bInfo) { f = Ga(a); e = 1 } else if (h == "p" && a.oFeatures.bPaginate) { f = Ha(a); e = 1 } else if (o.aoFeatures.length !==
0) { j = o.aoFeatures; t = 0; for (k = j.length; t < k; t++) if (h == j[t].cFeature) { if (f = j[t].fnInit(a)) e = 1; break } } if (e == 1 && f !== null) { if (typeof a.aanFeatures[h] != "object") a.aanFeatures[h] = []; a.aanFeatures[h].push(f); c.appendChild(f) }
} b.parentNode.replaceChild(a.nTableWrapper, b)
} function Fa(a) {
if (a.oScroll.sX === "" && a.oScroll.sY === "") return a.nTable; var b = p.createElement("div"), c = p.createElement("div"), d = p.createElement("div"), f = p.createElement("div"), e = p.createElement("div"), h = p.createElement("div"), j = a.nTable.cloneNode(false),
k = a.nTable.cloneNode(false), m = a.nTable.getElementsByTagName("thead")[0], t = a.nTable.getElementsByTagName("tfoot").length === 0 ? null : a.nTable.getElementsByTagName("tfoot")[0], q = typeof g.bJQueryUI != "undefined" && g.bJQueryUI ? o.oJUIClasses : o.oStdClasses; c.appendChild(d); e.appendChild(h); f.appendChild(a.nTable); b.appendChild(c); b.appendChild(f); d.appendChild(j); j.appendChild(m); if (t !== null) { b.appendChild(e); h.appendChild(k); k.appendChild(t) } b.className = q.sScrollWrapper; c.className = q.sScrollHead; d.className =
q.sScrollHeadInner; f.className = q.sScrollBody; e.className = q.sScrollFoot; h.className = q.sScrollFootInner; if (a.oScroll.bAutoCss) { c.style.overflow = "hidden"; c.style.position = "relative"; e.style.overflow = "hidden"; f.style.overflow = "auto" } c.style.border = "0"; c.style.width = "100%"; e.style.border = "0"; d.style.width = "150%"; j.removeAttribute("id"); j.style.marginLeft = "0"; a.nTable.style.marginLeft = "0"; if (t !== null) { k.removeAttribute("id"); k.style.marginLeft = "0" } d = i(">caption", a.nTable); h = 0; for (k = d.length; h < k; h++) j.appendChild(d[h]);
if (a.oScroll.sX !== "") { c.style.width = u(a.oScroll.sX); f.style.width = u(a.oScroll.sX); if (t !== null) e.style.width = u(a.oScroll.sX); i(f).scroll(function () { c.scrollLeft = this.scrollLeft; if (t !== null) e.scrollLeft = this.scrollLeft }) } if (a.oScroll.sY !== "") f.style.height = u(a.oScroll.sY); a.aoDrawCallback.push({ fn: Ia, sName: "scrolling" }); a.oScroll.bInfinite && i(f).scroll(function () {
if (!a.bDrawing) if (i(this).scrollTop() + i(this).height() > i(a.nTable).height() - a.oScroll.iLoadGap) if (a.fnDisplayEnd() < a.fnRecordsDisplay()) {
ja(a,
"next"); E(a); C(a)
}
}); a.nScrollHead = c; a.nScrollFoot = e; return b
} function Ia(a) {
var b = a.nScrollHead.getElementsByTagName("div")[0], c = b.getElementsByTagName("table")[0], d = a.nTable.parentNode, f, e, h, j, k, m, t, q, I = []; h = a.nTable.getElementsByTagName("thead"); h.length > 0 && a.nTable.removeChild(h[0]); if (a.nTFoot !== null) { k = a.nTable.getElementsByTagName("tfoot"); k.length > 0 && a.nTable.removeChild(k[0]) } h = a.nTHead.cloneNode(true); a.nTable.insertBefore(h, a.nTable.childNodes[0]); if (a.nTFoot !== null) {
k = a.nTFoot.cloneNode(true);
a.nTable.insertBefore(k, a.nTable.childNodes[1])
} if (a.oScroll.sX === "") { d.style.width = "100%"; b.parentNode.style.width = "100%" } var O = S(a, h); f = 0; for (e = O.length; f < e; f++) { t = Ja(a, f); O[f].style.width = a.aoColumns[t].sWidth } a.nTFoot !== null && P(function (B) { B.style.width = "" }, k.getElementsByTagName("tr")); f = i(a.nTable).outerWidth(); if (a.oScroll.sX === "") { a.nTable.style.width = "100%"; if (i.browser.msie && i.browser.version <= 7) a.nTable.style.width = u(i(a.nTable).outerWidth() - a.oScroll.iBarWidth) } else if (a.oScroll.sXInner !==
"") a.nTable.style.width = u(a.oScroll.sXInner); else if (f == i(d).width() && i(d).height() < i(a.nTable).height()) { a.nTable.style.width = u(f - a.oScroll.iBarWidth); if (i(a.nTable).outerWidth() > f - a.oScroll.iBarWidth) a.nTable.style.width = u(f) } else a.nTable.style.width = u(f); f = i(a.nTable).outerWidth(); if (a.oScroll.sX === "") { d.style.width = u(f + a.oScroll.iBarWidth); b.parentNode.style.width = u(f + a.oScroll.iBarWidth) } e = a.nTHead.getElementsByTagName("tr"); h = h.getElementsByTagName("tr"); P(function (B, F) {
m = B.style; m.paddingTop =
"0"; m.paddingBottom = "0"; m.borderTopWidth = "0"; m.borderBottomWidth = "0"; m.height = 0; q = i(B).width(); F.style.width = u(q); I.push(q)
}, h, e); i(h).height(0); if (a.nTFoot !== null) { j = k.getElementsByTagName("tr"); k = a.nTFoot.getElementsByTagName("tr"); P(function (B, F) { m = B.style; m.paddingTop = "0"; m.paddingBottom = "0"; m.borderTopWidth = "0"; m.borderBottomWidth = "0"; m.height = 0; q = i(B).width(); F.style.width = u(q); I.push(q) }, j, k); i(j).height(0) } P(function (B) { B.innerHTML = ""; B.style.width = u(I.shift()) }, h); a.nTFoot !== null && P(function (B) {
B.innerHTML =
""; B.style.width = u(I.shift())
}, j); if (i(a.nTable).outerWidth() < f) if (a.oScroll.sX === "") J(a, 1, "The table cannot fit into the current element which will cause column misalignment. It is suggested that you enable x-scrolling or increase the width the table has in which to be drawn"); else a.oScroll.sXInner !== "" && J(a, 1, "The table cannot fit into the current element which will cause column misalignment. It is suggested that you increase the sScrollXInner property to allow it to draw in a larger area, or simply remove that parameter to allow automatic calculation");
if (a.oScroll.sY === "") if (i.browser.msie && i.browser.version <= 7) d.style.height = u(a.nTable.offsetHeight + a.oScroll.iBarWidth); if (a.oScroll.sY !== "" && a.oScroll.bCollapse) { d.style.height = u(a.oScroll.sY); j = a.oScroll.sX !== "" && a.nTable.offsetWidth > d.offsetWidth ? a.oScroll.iBarWidth : 0; if (a.nTable.offsetHeight < d.offsetHeight) d.style.height = u(i(a.nTable).height() + j) } j = i(a.nTable).outerWidth(); c.style.width = u(j); b.style.width = u(j + a.oScroll.iBarWidth); if (a.nTFoot !== null) {
b = a.nScrollFoot.getElementsByTagName("div")[0];
c = b.getElementsByTagName("table")[0]; b.style.width = u(a.nTable.offsetWidth + a.oScroll.iBarWidth); c.style.width = u(a.nTable.offsetWidth)
} if (a.bSorted || a.bFiltered) d.scrollTop = 0
}
// sFilter
function ca(a) { if (a.oFeatures.bAutoWidth === false) return false; ea(a); for (var b = 0, c = a.aoColumns.length; b < c; b++) a.aoColumns[b].nTh.style.width = a.aoColumns[b].sWidth } function Da(a) {
var b = a.oLanguage.sSearch;
b = b.indexOf("_INPUT_") !== -1 ? b.replace("_INPUT_", '<input type="text" />') : b === "" ? '<input type="text" />' : b + ' <input type="text" />';
var c = p.createElement("div");
c.className = a.oClasses.sFilter;
c.innerHTML = "<div>" + b + "</div>";
a.sTableId !== "" && typeof a.aanFeatures.f == "undefined" && c.setAttribute("id", a.sTableId + "_filter");
b = i("input", c);
b.val(a.oPreviousSearch.sSearch.replace('"', "&quot;"));
b.bind("keyup.DT", function () {
for (var d = a.aanFeatures.f, f = 0, e = d.length;
f < e; f++)
d[f] != this.parentNode && i("input", d[f]).val(this.value);
this.value != a.oPreviousSearch.sSearch && M(a, {
sSearch: this.value, bRegex: a.oPreviousSearch.bRegex, bSmart: a.oPreviousSearch.bSmart
})
});
b.bind("keypress.DT", function (d) {
if (d.keyCode == 13) return false
});
return c
} function M(a, b, c) { Ka(a, b.sSearch, c, b.bRegex, b.bSmart); for (b = 0; b < a.aoPreSearchCols.length; b++) La(a, a.aoPreSearchCols[b].sSearch, b, a.aoPreSearchCols[b].bRegex, a.aoPreSearchCols[b].bSmart); o.afnFiltering.length !== 0 && Ma(a); a.bFiltered = true; a._iDisplayStart = 0; E(a); C(a); ka(a, 0) } function Ma(a) {
for (var b = o.afnFiltering, c = 0, d = b.length; c < d; c++) for (var f = 0, e = 0, h = a.aiDisplay.length; e < h; e++) {
var j = a.aiDisplay[e - f];
if (!b[c](a, da(a, j, "filter"),
j)) {
a.aiDisplay.splice(e - f, 1); f++
}
}
} function La(a, b, c, d, f) { if (b !== "") { var e = 0; b = la(b, d, f); for (d = a.aiDisplay.length - 1; d >= 0; d--) { f = ma(H(a, a.aiDisplay[d], c, "filter"), a.aoColumns[c].sType); if (!b.test(f)) { a.aiDisplay.splice(d, 1); e++ } } } } function Ka(a, b, c, d, f) {
var e = la(b, d, f); if (typeof c == "undefined" || c === null) c = 0; if (o.afnFiltering.length !== 0) c = 1; if (b.length <= 0) { a.aiDisplay.splice(0, a.aiDisplay.length); a.aiDisplay = a.aiDisplayMaster.slice() } else if (a.aiDisplay.length == a.aiDisplayMaster.length || a.oPreviousSearch.sSearch.length >
b.length || c == 1 || b.indexOf(a.oPreviousSearch.sSearch) !== 0) { a.aiDisplay.splice(0, a.aiDisplay.length); ka(a, 1); for (c = 0; c < a.aiDisplayMaster.length; c++) e.test(a.asDataSearch[c]) && a.aiDisplay.push(a.aiDisplayMaster[c]) } else { var h = 0; for (c = 0; c < a.asDataSearch.length; c++) if (!e.test(a.asDataSearch[c])) { a.aiDisplay.splice(c - h, 1); h++ } } a.oPreviousSearch.sSearch = b; a.oPreviousSearch.bRegex = d; a.oPreviousSearch.bSmart = f
} function ka(a, b) {
a.asDataSearch.splice(0, a.asDataSearch.length); b = typeof b != "undefined" && b == 1 ? a.aiDisplayMaster :
a.aiDisplay; for (var c = 0, d = b.length; c < d; c++) a.asDataSearch[c] = na(a, da(a, b[c], "filter"))
} function na(a, b) { var c = ""; if (typeof a.__nTmpFilter == "undefined") a.__nTmpFilter = p.createElement("div"); for (var d = a.__nTmpFilter, f = 0, e = a.aoColumns.length; f < e; f++) if (a.aoColumns[f].bSearchable) c += ma(b[f], a.aoColumns[f].sType) + " "; if (c.indexOf("&") !== -1) { d.innerHTML = c; c = d.textContent ? d.textContent : d.innerText; c = c.replace(/\n/g, " ").replace(/\r/g, "") } return c } function la(a, b, c) {
if (c) {
a = b ? a.split(" ") : oa(a).split(" ");
a = "^(?=.*?" + a.join(")(?=.*?") + ").*$"; return new RegExp(a, "i")
} else { a = b ? a : oa(a); return new RegExp(a, "i") }
} function ma(a, b) { if (typeof o.ofnSearch[b] == "function") return o.ofnSearch[b](a); else if (b == "html") return a.replace(/\n/g, " ").replace(/<.*?>/g, ""); else if (typeof a == "string") return a.replace(/\n/g, " "); else if (a === null) return ""; return a } function R(a, b) {
var c, d, f, e, h = [], j = [], k = o.oSort; d = a.aoData; var m = a.aoColumns; if (!a.oFeatures.bServerSide && (a.aaSorting.length !== 0 || a.aaSortingFixed !== null)) {
h = a.aaSortingFixed !==
null ? a.aaSortingFixed.concat(a.aaSorting) : a.aaSorting.slice(); for (c = 0; c < h.length; c++) { var t = h[c][0]; f = pa(a, t); e = a.aoColumns[t].sSortDataType; if (typeof o.afnSortData[e] != "undefined") { var q = o.afnSortData[e](a, t, f); f = 0; for (e = d.length; f < e; f++) N(a, f, t, q[f]) } } c = 0; for (d = a.aiDisplayMaster.length; c < d; c++) j[a.aiDisplayMaster[c]] = c; var I = h.length; a.aiDisplayMaster.sort(function (O, B) {
var F, qa; for (c = 0; c < I; c++) {
F = m[h[c][0]].iDataSort; qa = m[F].sType; F = k[(qa ? qa : "string") + "-" + h[c][1]](H(a, O, F, "sort"), H(a, B, F, "sort"));
if (F !== 0) return F
} return k["numeric-asc"](j[O], j[B])
})
} if ((typeof b == "undefined" || b) && !a.oFeatures.bDeferRender) T(a); a.bSorted = true; if (a.oFeatures.bFilter) M(a, a.oPreviousSearch, 1); else { a.aiDisplay = a.aiDisplayMaster.slice(); a._iDisplayStart = 0; E(a); C(a) }
} function ga(a, b, c, d) {
i(b).bind("click.DT", function (f) {
if (a.aoColumns[c].bSortable !== false) {
var e = function () {
var h, j; if (f.shiftKey) {
for (var k = false, m = 0; m < a.aaSorting.length; m++) if (a.aaSorting[m][0] == c) {
k = true; h = a.aaSorting[m][0]; j = a.aaSorting[m][2] +
1; if (typeof a.aoColumns[h].asSorting[j] == "undefined") a.aaSorting.splice(m, 1); else { a.aaSorting[m][1] = a.aoColumns[h].asSorting[j]; a.aaSorting[m][2] = j } break
} k === false && a.aaSorting.push([c, a.aoColumns[c].asSorting[0], 0])
} else if (a.aaSorting.length == 1 && a.aaSorting[0][0] == c) { h = a.aaSorting[0][0]; j = a.aaSorting[0][2] + 1; if (typeof a.aoColumns[h].asSorting[j] == "undefined") j = 0; a.aaSorting[0][1] = a.aoColumns[h].asSorting[j]; a.aaSorting[0][2] = j } else {
a.aaSorting.splice(0, a.aaSorting.length); a.aaSorting.push([c, a.aoColumns[c].asSorting[0],
0])
} R(a)
}; if (a.oFeatures.bProcessing) { K(a, true); setTimeout(function () { e(); a.oFeatures.bServerSide || K(a, false) }, 0) } else e(); typeof d == "function" && d(a)
}
})
} function T(a) {
var b, c, d, f, e, h = a.aoColumns.length, j = a.oClasses; for (b = 0; b < h; b++) a.aoColumns[b].bSortable && i(a.aoColumns[b].nTh).removeClass(j.sSortAsc + " " + j.sSortDesc + " " + a.aoColumns[b].sSortingClass); f = a.aaSortingFixed !== null ? a.aaSortingFixed.concat(a.aaSorting) : a.aaSorting.slice(); for (b = 0; b < a.aoColumns.length; b++) if (a.aoColumns[b].bSortable) {
e = a.aoColumns[b].sSortingClass;
d = -1; for (c = 0; c < f.length; c++) if (f[c][0] == b) { e = f[c][1] == "asc" ? j.sSortAsc : j.sSortDesc; d = c; break } i(a.aoColumns[b].nTh).addClass(e); if (a.bJUI) { c = i("span", a.aoColumns[b].nTh); c.removeClass(j.sSortJUIAsc + " " + j.sSortJUIDesc + " " + j.sSortJUI + " " + j.sSortJUIAscAllowed + " " + j.sSortJUIDescAllowed); c.addClass(d == -1 ? a.aoColumns[b].sSortingClassJUI : f[d][1] == "asc" ? j.sSortJUIAsc : j.sSortJUIDesc) }
} else i(a.aoColumns[b].nTh).addClass(a.aoColumns[b].sSortingClass); e = j.sSortColumn; if (a.oFeatures.bSort && a.oFeatures.bSortClasses) {
d =
Q(a); if (a.oFeatures.bDeferRender) i(d).removeClass(e + "1 " + e + "2 " + e + "3"); else if (d.length >= h) for (b = 0; b < h; b++) if (d[b].className.indexOf(e + "1") != -1) { c = 0; for (a = d.length / h; c < a; c++) d[h * c + b].className = i.trim(d[h * c + b].className.replace(e + "1", "")) } else if (d[b].className.indexOf(e + "2") != -1) { c = 0; for (a = d.length / h; c < a; c++) d[h * c + b].className = i.trim(d[h * c + b].className.replace(e + "2", "")) } else if (d[b].className.indexOf(e + "3") != -1) {
c = 0; for (a = d.length / h; c < a; c++) d[h * c + b].className = i.trim(d[h * c + b].className.replace(" " +
e + "3", ""))
} j = 1; var k; for (b = 0; b < f.length; b++) { k = parseInt(f[b][0], 10); c = 0; for (a = d.length / h; c < a; c++) d[h * c + k].className += " " + e + j; j < 3 && j++ }
}
} function Ha(a) { if (a.oScroll.bInfinite) return null; var b = p.createElement("div"); b.className = a.oClasses.sPaging + a.sPaginationType; o.oPagination[a.sPaginationType].fnInit(a, b, function (c) { E(c); C(c) }); typeof a.aanFeatures.p == "undefined" && a.aoDrawCallback.push({ fn: function (c) { o.oPagination[c.sPaginationType].fnUpdate(c, function (d) { E(d); C(d) }) }, sName: "pagination" }); return b }
function ja(a, b) {
var c = a._iDisplayStart; if (b == "first") a._iDisplayStart = 0; else if (b == "previous") { a._iDisplayStart = a._iDisplayLength >= 0 ? a._iDisplayStart - a._iDisplayLength : 0; if (a._iDisplayStart < 0) a._iDisplayStart = 0 } else if (b == "next") if (a._iDisplayLength >= 0) { if (a._iDisplayStart + a._iDisplayLength < a.fnRecordsDisplay()) a._iDisplayStart += a._iDisplayLength } else a._iDisplayStart = 0; else if (b == "last") if (a._iDisplayLength >= 0) { b = parseInt((a.fnRecordsDisplay() - 1) / a._iDisplayLength, 10) + 1; a._iDisplayStart = (b - 1) * a._iDisplayLength } else a._iDisplayStart =
0; else J(a, 0, "Unknown paging action: " + b); return c != a._iDisplayStart
} function Ga(a) { var b = p.createElement("div"); b.className = a.oClasses.sInfo; if (typeof a.aanFeatures.i == "undefined") { a.aoDrawCallback.push({ fn: Na, sName: "information" }); a.sTableId !== "" && b.setAttribute("id", a.sTableId + "_info") } return b } function Na(a) {
if (!(!a.oFeatures.bInfo || a.aanFeatures.i.length === 0)) {
var b = a._iDisplayStart + 1, c = a.fnDisplayEnd(), d = a.fnRecordsTotal(), f = a.fnRecordsDisplay(), e = a.fnFormatNumber(b), h = a.fnFormatNumber(c), j =
a.fnFormatNumber(d), k = a.fnFormatNumber(f); if (a.oScroll.bInfinite) e = a.fnFormatNumber(1); e = a.fnRecordsDisplay() === 0 && a.fnRecordsDisplay() == a.fnRecordsTotal() ? a.oLanguage.sInfoEmpty + a.oLanguage.sInfoPostFix : a.fnRecordsDisplay() === 0 ? a.oLanguage.sInfoEmpty + " " + a.oLanguage.sInfoFiltered.replace("_MAX_", j) + a.oLanguage.sInfoPostFix : a.fnRecordsDisplay() == a.fnRecordsTotal() ? a.oLanguage.sInfo.replace("_START_", e).replace("_END_", h).replace("_TOTAL_", k) + a.oLanguage.sInfoPostFix : a.oLanguage.sInfo.replace("_START_",
e).replace("_END_", h).replace("_TOTAL_", k) + " " + a.oLanguage.sInfoFiltered.replace("_MAX_", a.fnFormatNumber(a.fnRecordsTotal())) + a.oLanguage.sInfoPostFix; if (a.oLanguage.fnInfoCallback !== null) e = a.oLanguage.fnInfoCallback(a, b, c, d, f, e); a = a.aanFeatures.i; b = 0; for (c = a.length; b < c; b++) i(a[b]).html(e)
}
} function Ca(a) {
if (a.oScroll.bInfinite) return null; var b = '<select size="1" ' + (a.sTableId === "" ? "" : 'name="' + a.sTableId + '_length"') + ">", c, d; if (a.aLengthMenu.length == 2 && typeof a.aLengthMenu[0] == "object" && typeof a.aLengthMenu[1] ==
"object") { c = 0; for (d = a.aLengthMenu[0].length; c < d; c++) b += '<option value="' + a.aLengthMenu[0][c] + '">' + a.aLengthMenu[1][c] + "</option>" } else { c = 0; for (d = a.aLengthMenu.length; c < d; c++) b += '<option value="' + a.aLengthMenu[c] + '">' + a.aLengthMenu[c] + "</option>" } b += "</select>"; var f = p.createElement("div"); a.sTableId !== "" && typeof a.aanFeatures.l == "undefined" && f.setAttribute("id", a.sTableId + "_length"); f.className = a.oClasses.sLength; f.innerHTML = "<label>" + a.oLanguage.sLengthMenu.replace("_MENU_", b) + "</label>"; i('select option[value="' +
a._iDisplayLength + '"]', f).attr("selected", true); i("select", f).bind("change.DT", function () { var e = i(this).val(), h = a.aanFeatures.l; c = 0; for (d = h.length; c < d; c++) h[c] != this.parentNode && i("select", h[c]).val(e); a._iDisplayLength = parseInt(e, 10); E(a); if (a.fnDisplayEnd() == a.fnRecordsDisplay()) { a._iDisplayStart = a.fnDisplayEnd() - a._iDisplayLength; if (a._iDisplayStart < 0) a._iDisplayStart = 0 } if (a._iDisplayLength == -1) a._iDisplayStart = 0; C(a) }); return f
} function Ea(a) {
var b = p.createElement("div"); a.sTableId !== "" && typeof a.aanFeatures.r ==
"undefined" && b.setAttribute("id", a.sTableId + "_processing"); b.innerHTML = a.oLanguage.sProcessing; b.className = a.oClasses.sProcessing; a.nTable.parentNode.insertBefore(b, a.nTable); return b
} function K(a, b) { if (a.oFeatures.bProcessing) { a = a.aanFeatures.r; for (var c = 0, d = a.length; c < d; c++) a[c].style.visibility = b ? "visible" : "hidden" } } function Ja(a, b) { for (var c = -1, d = 0; d < a.aoColumns.length; d++) { a.aoColumns[d].bVisible === true && c++; if (c == b) return d } return null } function pa(a, b) {
for (var c = -1, d = 0; d < a.aoColumns.length; d++) {
a.aoColumns[d].bVisible ===
true && c++; if (d == b) return a.aoColumns[d].bVisible === true ? c : null
} return null
} function U(a, b) { var c, d; c = a._iDisplayStart; for (d = a._iDisplayEnd; c < d; c++) if (a.aoData[a.aiDisplay[c]].nTr == b) return a.aiDisplay[c]; c = 0; for (d = a.aoData.length; c < d; c++) if (a.aoData[c].nTr == b) return c; return null } function X(a) { for (var b = 0, c = 0; c < a.aoColumns.length; c++) a.aoColumns[c].bVisible === true && b++; return b } function E(a) {
a._iDisplayEnd = a.oFeatures.bPaginate === false ? a.aiDisplay.length : a._iDisplayStart + a._iDisplayLength > a.aiDisplay.length ||
a._iDisplayLength == -1 ? a.aiDisplay.length : a._iDisplayStart + a._iDisplayLength
} function Oa(a, b) { if (!a || a === null || a === "") return 0; if (typeof b == "undefined") b = p.getElementsByTagName("body")[0]; var c = p.createElement("div"); c.style.width = u(a); b.appendChild(c); a = c.offsetWidth; b.removeChild(c); return a } function ea(a) {
var b = 0, c, d = 0, f = a.aoColumns.length, e, h = i("th", a.nTHead); for (e = 0; e < f; e++) if (a.aoColumns[e].bVisible) {
d++; if (a.aoColumns[e].sWidth !== null) {
c = Oa(a.aoColumns[e].sWidthOrig, a.nTable.parentNode); if (c !==
null) a.aoColumns[e].sWidth = u(c); b++
}
} if (f == h.length && b === 0 && d == f && a.oScroll.sX === "" && a.oScroll.sY === "") for (e = 0; e < a.aoColumns.length; e++) { c = i(h[e]).width(); if (c !== null) a.aoColumns[e].sWidth = u(c) } else {
b = a.nTable.cloneNode(false); e = a.nTHead.cloneNode(true); d = p.createElement("tbody"); c = p.createElement("tr"); b.removeAttribute("id"); b.appendChild(e); if (a.nTFoot !== null) { b.appendChild(a.nTFoot.cloneNode(true)); P(function (k) { k.style.width = "" }, b.getElementsByTagName("tr")) } b.appendChild(d); d.appendChild(c);
d = i("thead th", b); if (d.length === 0) d = i("tbody tr:eq(0)>td", b); h = S(a, e); for (e = d = 0; e < f; e++) { var j = a.aoColumns[e]; if (j.bVisible && j.sWidthOrig !== null && j.sWidthOrig !== "") h[e - d].style.width = u(j.sWidthOrig); else if (j.bVisible) h[e - d].style.width = ""; else d++ } for (e = 0; e < f; e++) if (a.aoColumns[e].bVisible) { d = Pa(a, e); if (d !== null) { d = d.cloneNode(true); if (a.aoColumns[e].sContentPadding !== "") d.innerHTML += a.aoColumns[e].sContentPadding; c.appendChild(d) } } f = a.nTable.parentNode; f.appendChild(b); if (a.oScroll.sX !== "" && a.oScroll.sXInner !==
"") b.style.width = u(a.oScroll.sXInner); else if (a.oScroll.sX !== "") { b.style.width = ""; if (i(b).width() < f.offsetWidth) b.style.width = u(f.offsetWidth) } else if (a.oScroll.sY !== "") b.style.width = u(f.offsetWidth); b.style.visibility = "hidden"; Qa(a, b); f = i("tbody tr:eq(0)", b).children(); if (f.length === 0) f = S(a, i("thead", b)[0]); if (a.oScroll.sX !== "") {
for (e = d = c = 0; e < a.aoColumns.length; e++) if (a.aoColumns[e].bVisible) {
c += a.aoColumns[e].sWidthOrig === null ? i(f[d]).outerWidth() : parseInt(a.aoColumns[e].sWidth.replace("px", ""),
10) + (i(f[d]).outerWidth() - i(f[d]).width()); d++
} b.style.width = u(c); a.nTable.style.width = u(c)
} for (e = d = 0; e < a.aoColumns.length; e++) if (a.aoColumns[e].bVisible) { c = i(f[d]).width(); if (c !== null && c > 0) a.aoColumns[e].sWidth = u(c); d++ } a.nTable.style.width = u(i(b).outerWidth()); b.parentNode.removeChild(b)
}
} function Qa(a, b) { if (a.oScroll.sX === "" && a.oScroll.sY !== "") { i(b).width(); b.style.width = u(i(b).outerWidth() - a.oScroll.iBarWidth) } else if (a.oScroll.sX !== "") b.style.width = u(i(b).outerWidth()) } function Pa(a, b) {
var c =
Ra(a, b); if (c < 0) return null; if (a.aoData[c].nTr === null) { var d = p.createElement("td"); d.innerHTML = H(a, c, b, ""); return d } return Q(a, c)[b]
} function Ra(a, b) { for (var c = -1, d = -1, f = 0; f < a.aoData.length; f++) { var e = H(a, f, b, "display") + ""; e = e.replace(/<.*?>/g, ""); if (e.length > c) { c = e.length; d = f } } return d } function u(a) { if (a === null) return "0px"; if (typeof a == "number") { if (a < 0) return "0px"; return a + "px" } var b = a.charCodeAt(a.length - 1); if (b < 48 || b > 57) return a; return a + "px" } function Va(a, b) {
if (a.length != b.length) return 1; for (var c =
0; c < a.length; c++) if (a[c] != b[c]) return 2; return 0
} function fa(a) { for (var b = o.aTypes, c = b.length, d = 0; d < c; d++) { var f = b[d](a); if (f !== null) return f } return "string" } function A(a) { for (var b = 0; b < D.length; b++) if (D[b].nTable == a) return D[b]; return null } function aa(a) { for (var b = [], c = a.aoData.length, d = 0; d < c; d++) b.push(a.aoData[d]._aData); return b } function $(a) { for (var b = [], c = 0, d = a.aoData.length; c < d; c++) a.aoData[c].nTr !== null && b.push(a.aoData[c].nTr); return b } function Q(a, b) {
var c = [], d, f, e, h, j; f = 0; var k = a.aoData.length;
if (typeof b != "undefined") { f = b; k = b + 1 } for (f = f; f < k; f++) { j = a.aoData[f]; if (j.nTr !== null) { b = []; e = 0; for (h = j.nTr.childNodes.length; e < h; e++) { d = j.nTr.childNodes[e].nodeName.toLowerCase(); if (d == "td" || d == "th") b.push(j.nTr.childNodes[e]) } e = d = 0; for (h = a.aoColumns.length; e < h; e++) if (a.aoColumns[e].bVisible) c.push(b[e - d]); else { c.push(j._anHidden[e]); d++ } } } return c
} function oa(a) { return a.replace(new RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^)", "g"), "\\$1") } function ra(a, b) {
for (var c = -1, d =
0, f = a.length; d < f; d++) if (a[d] == b) c = d; else a[d] > b && a[d]--; c != -1 && a.splice(c, 1)
} function Ba(a, b) { b = b.split(","); for (var c = [], d = 0, f = a.aoColumns.length; d < f; d++) for (var e = 0; e < f; e++) if (a.aoColumns[d].sName == b[e]) { c.push(e); break } return c } function ha(a) { for (var b = "", c = 0, d = a.aoColumns.length; c < d; c++) b += a.aoColumns[c].sName + ","; if (b.length == d) return ""; return b.slice(0, -1) } function J(a, b, c) {
a = a.sTableId === "" ? "DataTables warning: " + c : "DataTables warning (table id = '" + a.sTableId + "'): " + c; if (b === 0) if (o.sErrMode ==
"alert") alert(a); else throw a; else typeof console != "undefined" && typeof console.log != "undefined" && console.log(a)
} function ia(a) { a.aoData.splice(0, a.aoData.length); a.aiDisplayMaster.splice(0, a.aiDisplayMaster.length); a.aiDisplay.splice(0, a.aiDisplay.length); E(a) } function sa(a) {
if (!(!a.oFeatures.bStateSave || typeof a.bDestroying != "undefined")) {
var b, c, d, f = "{"; f += '"iCreate":' + (new Date).getTime() + ","; f += '"iStart":' + (a.oScroll.bInfinite ? 0 : a._iDisplayStart) + ","; f += '"iEnd":' + (a.oScroll.bInfinite ? a._iDisplayLength :
a._iDisplayEnd) + ","; f += '"iLength":' + a._iDisplayLength + ","; f += '"sFilter":"' + encodeURIComponent(a.oPreviousSearch.sSearch) + '",'; f += '"sFilterEsc":' + !a.oPreviousSearch.bRegex + ","; f += '"aaSorting":[ '; for (b = 0; b < a.aaSorting.length; b++) f += "[" + a.aaSorting[b][0] + ',"' + a.aaSorting[b][1] + '"],'; f = f.substring(0, f.length - 1); f += "],"; f += '"aaSearchCols":[ '; for (b = 0; b < a.aoPreSearchCols.length; b++) f += '["' + encodeURIComponent(a.aoPreSearchCols[b].sSearch) + '",' + !a.aoPreSearchCols[b].bRegex + "],"; f = f.substring(0, f.length -
1); f += "],"; f += '"abVisCols":[ '; for (b = 0; b < a.aoColumns.length; b++) f += a.aoColumns[b].bVisible + ","; f = f.substring(0, f.length - 1); f += "]"; b = 0; for (c = a.aoStateSave.length; b < c; b++) { d = a.aoStateSave[b].fn(a, f); if (d !== "") f = d } f += "}"; Sa(a.sCookiePrefix + a.sInstance, f, a.iCookieDuration, a.sCookiePrefix, a.fnCookieCallback)
}
} function Ta(a, b) {
if (a.oFeatures.bStateSave) {
var c, d, f; d = ta(a.sCookiePrefix + a.sInstance); if (d !== null && d !== "") {
try { c = typeof i.parseJSON == "function" ? i.parseJSON(d.replace(/'/g, '"')) : eval("(" + d + ")") } catch (e) { return } d =
0; for (f = a.aoStateLoad.length; d < f; d++) if (!a.aoStateLoad[d].fn(a, c)) return; a.oLoadedState = i.extend(true, {}, c); a._iDisplayStart = c.iStart; a.iInitDisplayStart = c.iStart; a._iDisplayEnd = c.iEnd; a._iDisplayLength = c.iLength; a.oPreviousSearch.sSearch = decodeURIComponent(c.sFilter); a.aaSorting = c.aaSorting.slice(); a.saved_aaSorting = c.aaSorting.slice(); if (typeof c.sFilterEsc != "undefined") a.oPreviousSearch.bRegex = !c.sFilterEsc; if (typeof c.aaSearchCols != "undefined") for (d = 0; d < c.aaSearchCols.length; d++) a.aoPreSearchCols[d] =
{ sSearch: decodeURIComponent(c.aaSearchCols[d][0]), bRegex: !c.aaSearchCols[d][1] }; if (typeof c.abVisCols != "undefined") { b.saved_aoColumns = []; for (d = 0; d < c.abVisCols.length; d++) { b.saved_aoColumns[d] = {}; b.saved_aoColumns[d].bVisible = c.abVisCols[d] } }
}
}
} function Sa(a, b, c, d, f) {
var e = new Date; e.setTime(e.getTime() + c * 1E3); c = wa.location.pathname.split("/"); a = a + "_" + c.pop().replace(/[\/:]/g, "").toLowerCase(); var h; if (f !== null) {
h = typeof i.parseJSON == "function" ? i.parseJSON(b) : eval("(" + b + ")"); b = f(a, h, e.toGMTString(),
c.join("/") + "/")
} else b = a + "=" + encodeURIComponent(b) + "; expires=" + e.toGMTString() + "; path=" + c.join("/") + "/"; f = ""; e = 9999999999999; if ((ta(a) !== null ? p.cookie.length : b.length + p.cookie.length) + 10 > 4096) {
a = p.cookie.split(";"); for (var j = 0, k = a.length; j < k; j++) if (a[j].indexOf(d) != -1) { var m = a[j].split("="); try { h = eval("(" + decodeURIComponent(m[1]) + ")") } catch (t) { continue } if (typeof h.iCreate != "undefined" && h.iCreate < e) { f = m[0]; e = h.iCreate } } if (f !== "") p.cookie = f + "=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=" + c.join("/") +
"/"
} p.cookie = b
} function ta(a) { var b = wa.location.pathname.split("/"); a = a + "_" + b[b.length - 1].replace(/[\/:]/g, "").toLowerCase() + "="; b = p.cookie.split(";"); for (var c = 0; c < b.length; c++) { for (var d = b[c]; d.charAt(0) == " "; ) d = d.substring(1, d.length); if (d.indexOf(a) === 0) return decodeURIComponent(d.substring(a.length, d.length)) } return null } function W(a, b) {
b = b.getElementsByTagName("tr"); var c, d, f, e, h, j, k, m, t = function (O, B, F) { for (; typeof O[B][F] != "undefined"; ) F++; return F }; a.splice(0, a.length); d = 0; for (j = b.length; d <
j; d++) a.push([]); d = 0; for (j = b.length; d < j; d++) { f = 0; for (k = b[d].childNodes.length; f < k; f++) { c = b[d].childNodes[f]; if (c.nodeName.toUpperCase() == "TD" || c.nodeName.toUpperCase() == "TH") { var q = c.getAttribute("colspan") * 1, I = c.getAttribute("rowspan") * 1; q = !q || q === 0 || q === 1 ? 1 : q; I = !I || I === 0 || I === 1 ? 1 : I; m = t(a, d, 0); for (h = 0; h < q; h++) for (e = 0; e < I; e++) { a[d + e][m + h] = { cell: c, unique: q == 1 ? true : false }; a[d + e].nTr = b[d] } } } }
} function S(a, b, c) {
var d = []; if (typeof c == "undefined") { c = a.aoHeader; if (typeof b != "undefined") { c = []; W(c, b) } } b = 0;
for (var f = c.length; b < f; b++) for (var e = 0, h = c[b].length; e < h; e++) if (c[b][e].unique && (typeof d[e] == "undefined" || !a.bSortCellsTop)) d[e] = c[b][e].cell; return d
} function Ua() {
var a = p.createElement("p"), b = a.style; b.width = "100%"; b.height = "200px"; var c = p.createElement("div"); b = c.style; b.position = "absolute"; b.top = "0px"; b.left = "0px"; b.visibility = "hidden"; b.width = "200px"; b.height = "150px"; b.overflow = "hidden"; c.appendChild(a); p.body.appendChild(c); b = a.offsetWidth; c.style.overflow = "scroll"; a = a.offsetWidth; if (b == a) a =
c.clientWidth; p.body.removeChild(c); return b - a
} function P(a, b, c) { for (var d = 0, f = b.length; d < f; d++) for (var e = 0, h = b[d].childNodes.length; e < h; e++) if (b[d].childNodes[e].nodeType == 1) typeof c != "undefined" ? a(b[d].childNodes[e], c[d].childNodes[e]) : a(b[d].childNodes[e]) } function n(a, b, c, d) { if (typeof d == "undefined") d = c; if (typeof b[c] != "undefined") a[d] = b[c] } function da(a, b, c) { for (var d = [], f = 0, e = a.aoColumns.length; f < e; f++) d.push(H(a, b, f, c)); return d } function H(a, b, c, d) {
var f = a.aoColumns[c]; if ((c = f.fnGetData(a.aoData[b]._aData)) ===
undefined) { if (a.iDrawError != a.iDraw && f.sDefaultContent === null) { J(a, 0, "Requested unknown parameter '" + f.mDataProp + "' from the data source for row " + b); a.iDrawError = a.iDraw } return f.sDefaultContent } if (c === null && f.sDefaultContent !== null) c = f.sDefaultContent; if (d == "display" && c === null) return ""; return c
} function N(a, b, c, d) { a.aoColumns[c].fnSetData(a.aoData[b]._aData, d) } function Z(a) {
if (a === null) return function () { return null }; else if (typeof a == "function") return function (c) { return a(c) }; else if (typeof a ==
"string" && a.indexOf(".") != -1) { var b = a.split("."); return b.length == 2 ? function (c) { return c[b[0]][b[1]] } : b.length == 3 ? function (c) { return c[b[0]][b[1]][b[2]] } : function (c) { for (var d = 0, f = b.length; d < f; d++) c = c[b[d]]; return c } } else return function (c) { return c[a] }
} function ya(a) {
if (a === null) return function () { }; else if (typeof a == "function") return function (c, d) { return a(c, d) }; else if (typeof a == "string" && a.indexOf(".") != -1) {
var b = a.split("."); return b.length == 2 ? function (c, d) { c[b[0]][b[1]] = d } : b.length == 3 ? function (c,
d) { c[b[0]][b[1]][b[2]] = d } : function (c, d) { for (var f = 0, e = b.length - 1; f < e; f++) c = c[b[f]]; c[b[b.length - 1]] = d }
} else return function (c, d) { c[a] = d }
} this.oApi = {}; this.fnDraw = function (a) { var b = A(this[o.iApiIndex]); if (typeof a != "undefined" && a === false) { E(b); C(b) } else ba(b) }; this.fnFilter = function (a, b, c, d, f) {
var e = A(this[o.iApiIndex]); if (e.oFeatures.bFilter) {
if (typeof c == "undefined") c = false; if (typeof d == "undefined") d = true; if (typeof f == "undefined") f = true; if (typeof b == "undefined" || b === null) {
M(e, { sSearch: a, bRegex: c,
bSmart: d
}, 1); if (f && typeof e.aanFeatures.f != "undefined") { b = e.aanFeatures.f; c = 0; for (d = b.length; c < d; c++) i("input", b[c]).val(a) }
} else { e.aoPreSearchCols[b].sSearch = a; e.aoPreSearchCols[b].bRegex = c; e.aoPreSearchCols[b].bSmart = d; M(e, e.oPreviousSearch, 1) }
}
}; this.fnSettings = function () { return A(this[o.iApiIndex]) }; this.fnVersionCheck = o.fnVersionCheck; this.fnSort = function (a) { var b = A(this[o.iApiIndex]); b.aaSorting = a; R(b) }; this.fnSortListener = function (a, b, c) { ga(A(this[o.iApiIndex]), a, b, c) }; this.fnAddData = function (a,
b) { if (a.length === 0) return []; var c = [], d, f = A(this[o.iApiIndex]); if (typeof a[0] == "object") for (var e = 0; e < a.length; e++) { d = v(f, a[e]); if (d == -1) return c; c.push(d) } else { d = v(f, a); if (d == -1) return c; c.push(d) } f.aiDisplay = f.aiDisplayMaster.slice(); if (typeof b == "undefined" || b) ba(f); return c }; this.fnDeleteRow = function (a, b, c) {
var d = A(this[o.iApiIndex]); a = typeof a == "object" ? U(d, a) : a; var f = d.aoData.splice(a, 1), e = i.inArray(a, d.aiDisplay); d.asDataSearch.splice(e, 1); ra(d.aiDisplayMaster, a); ra(d.aiDisplay, a); typeof b ==
"function" && b.call(this, d, f); if (d._iDisplayStart >= d.aiDisplay.length) { d._iDisplayStart -= d._iDisplayLength; if (d._iDisplayStart < 0) d._iDisplayStart = 0 } if (typeof c == "undefined" || c) { E(d); C(d) } return f
}; this.fnClearTable = function (a) { var b = A(this[o.iApiIndex]); ia(b); if (typeof a == "undefined" || a) C(b) }; this.fnOpen = function (a, b, c) {
var d = A(this[o.iApiIndex]); this.fnClose(a); var f = p.createElement("tr"), e = p.createElement("td"); f.appendChild(e); e.className = c; e.colSpan = X(d); if (typeof b.jquery != "undefined" || typeof b ==
"object") e.appendChild(b); else e.innerHTML = b; b = i("tr", d.nTBody); i.inArray(a, b) != -1 && i(f).insertAfter(a); d.aoOpenRows.push({ nTr: f, nParent: a }); return f
}; this.fnClose = function (a) { for (var b = A(this[o.iApiIndex]), c = 0; c < b.aoOpenRows.length; c++) if (b.aoOpenRows[c].nParent == a) { (a = b.aoOpenRows[c].nTr.parentNode) && a.removeChild(b.aoOpenRows[c].nTr); b.aoOpenRows.splice(c, 1); return 0 } return 1 }; this.fnGetData = function (a, b) {
var c = A(this[o.iApiIndex]); if (typeof a != "undefined") {
a = typeof a == "object" ? U(c, a) : a; if (typeof b !=
"undefined") return H(c, a, b, ""); return typeof c.aoData[a] != "undefined" ? c.aoData[a]._aData : null
} return aa(c)
}; this.fnGetNodes = function (a) { var b = A(this[o.iApiIndex]); if (typeof a != "undefined") return typeof b.aoData[a] != "undefined" ? b.aoData[a].nTr : null; return $(b) }; this.fnGetPosition = function (a) { var b = A(this[o.iApiIndex]), c = a.nodeName.toUpperCase(); if (c == "TR") return U(b, a); else if (c == "TD" || c == "TH") { c = U(b, a.parentNode); for (var d = Q(b, c), f = 0; f < b.aoColumns.length; f++) if (d[f] == a) return [c, pa(b, f), f] } return null };
this.fnUpdate = function (a, b, c, d, f) {
var e = A(this[o.iApiIndex]); b = typeof b == "object" ? U(e, b) : b; if (i.isArray(a) && typeof a == "object") { e.aoData[b]._aData = a.slice(); for (c = 0; c < e.aoColumns.length; c++) this.fnUpdate(H(e, b, c), b, c, false, false) } else if (typeof a == "object") { e.aoData[b]._aData = i.extend(true, {}, a); for (c = 0; c < e.aoColumns.length; c++) this.fnUpdate(H(e, b, c), b, c, false, false) } else {
a = a; N(e, b, c, a); if (e.aoColumns[c].fnRender !== null) {
a = e.aoColumns[c].fnRender({ iDataRow: b, iDataColumn: c, aData: e.aoData[b]._aData,
oSettings: e
}); e.aoColumns[c].bUseRendered && N(e, b, c, a)
} if (e.aoData[b].nTr !== null) Q(e, b)[c].innerHTML = a
} c = i.inArray(b, e.aiDisplay); e.asDataSearch[c] = na(e, da(e, b, "filter")); if (typeof f == "undefined" || f) ca(e); if (typeof d == "undefined" || d) ba(e); return 0
}; this.fnSetColumnVis = function (a, b, c) {
var d = A(this[o.iApiIndex]), f, e; e = d.aoColumns.length; var h, j; if (d.aoColumns[a].bVisible != b) {
if (b) {
for (f = j = 0; f < a; f++) d.aoColumns[f].bVisible && j++; j = j >= X(d); if (!j) for (f = a; f < e; f++) if (d.aoColumns[f].bVisible) { h = f; break } f = 0;
for (e = d.aoData.length; f < e; f++) if (d.aoData[f].nTr !== null) j ? d.aoData[f].nTr.appendChild(d.aoData[f]._anHidden[a]) : d.aoData[f].nTr.insertBefore(d.aoData[f]._anHidden[a], Q(d, f)[h])
} else { f = 0; for (e = d.aoData.length; f < e; f++) if (d.aoData[f].nTr !== null) { h = Q(d, f)[a]; d.aoData[f]._anHidden[a] = h; h.parentNode.removeChild(h) } } d.aoColumns[a].bVisible = b; L(d, d.aoHeader); d.nTFoot && L(d, d.aoFooter); f = 0; for (e = d.aoOpenRows.length; f < e; f++) d.aoOpenRows[f].nTr.colSpan = X(d); if (typeof c == "undefined" || c) { ca(d); C(d) } sa(d)
}
}; this.fnPageChange =
function (a, b) { var c = A(this[o.iApiIndex]); ja(c, a); E(c); if (typeof b == "undefined" || b) C(c) }; this.fnDestroy = function () {
var a = A(this[o.iApiIndex]), b = a.nTableWrapper.parentNode, c = a.nTBody, d, f; a.bDestroying = true; d = 0; for (f = a.aoColumns.length; d < f; d++) a.aoColumns[d].bVisible === false && this.fnSetColumnVis(d, true); i(a.nTableWrapper).find("*").andSelf().unbind(".DT"); i("tbody>tr>td." + a.oClasses.sRowEmpty, a.nTable).parent().remove(); if (a.nTable != a.nTHead.parentNode) { i(">thead", a.nTable).remove(); a.nTable.appendChild(a.nTHead) } if (a.nTFoot &&
a.nTable != a.nTFoot.parentNode) { i(">tfoot", a.nTable).remove(); a.nTable.appendChild(a.nTFoot) } a.nTable.parentNode.removeChild(a.nTable); i(a.nTableWrapper).remove(); a.aaSorting = []; a.aaSortingFixed = []; T(a); i($(a)).removeClass(a.asStripClasses.join(" ")); if (a.bJUI) {
i("th", a.nTHead).removeClass([o.oStdClasses.sSortable, o.oJUIClasses.sSortableAsc, o.oJUIClasses.sSortableDesc, o.oJUIClasses.sSortableNone].join(" ")); i("th span." + o.oJUIClasses.sSortIcon, a.nTHead).remove(); i("th", a.nTHead).each(function () {
var e =
i("div." + o.oJUIClasses.sSortJUIWrapper, this), h = e.contents(); i(this).append(h); e.remove()
})
} else i("th", a.nTHead).removeClass([o.oStdClasses.sSortable, o.oStdClasses.sSortableAsc, o.oStdClasses.sSortableDesc, o.oStdClasses.sSortableNone].join(" ")); a.nTableReinsertBefore ? b.insertBefore(a.nTable, a.nTableReinsertBefore) : b.appendChild(a.nTable); d = 0; for (f = a.aoData.length; d < f; d++) a.aoData[d].nTr !== null && c.appendChild(a.aoData[d].nTr); if (a.oFeatures.bAutoWidth === true) a.nTable.style.width = u(a.sDestroyWidth);
i(">tr:even", c).addClass(a.asDestoryStrips[0]); i(">tr:odd", c).addClass(a.asDestoryStrips[1]); d = 0; for (f = D.length; d < f; d++) D[d] == a && D.splice(d, 1); a = null
};
this.fnAdjustColumnSizing = function (a) { var b = A(this[o.iApiIndex]); ca(b); if (typeof a == "undefined" || a) this.fnDraw(false); else if (b.oScroll.sX !== "" || b.oScroll.sY !== "") this.oApi._fnScrollDraw(b) }; for (var ua in o.oApi) if (ua) this[ua] = r(ua); this.oApi._fnExternApiFunc = r; this.oApi._fnInitalise = s; this.oApi._fnInitComplete = w; this.oApi._fnLanguageProcess = y; this.oApi._fnAddColumn =
G;
this.oApi._fnColumnOptions = x; this.oApi._fnAddData = v; this.oApi._fnCreateTr = z; this.oApi._fnGatherData = Y; this.oApi._fnBuildHead = V; this.oApi._fnDrawHead = L; this.oApi._fnDraw = C; this.oApi._fnReDraw = ba; this.oApi._fnAjaxUpdate = za; this.oApi._fnAjaxUpdateDraw = Aa; this.oApi._fnAddOptionsHtml = xa; this.oApi._fnFeatureHtmlTable = Fa; this.oApi._fnScrollDraw = Ia; this.oApi._fnAjustColumnSizing = ca; this.oApi._fnFeatureHtmlFilter = Da; this.oApi._fnFilterComplete = M; this.oApi._fnFilterCustom = Ma; this.oApi._fnFilterColumn = La;
this.oApi._fnFilter = Ka; this.oApi._fnBuildSearchArray = ka; this.oApi._fnBuildSearchRow = na; this.oApi._fnFilterCreateSearch = la; this.oApi._fnDataToSearch = ma; this.oApi._fnSort = R; this.oApi._fnSortAttachListener = ga; this.oApi._fnSortingClasses = T; this.oApi._fnFeatureHtmlPaginate = Ha; this.oApi._fnPageChange = ja; this.oApi._fnFeatureHtmlInfo = Ga; this.oApi._fnUpdateInfo = Na; this.oApi._fnFeatureHtmlLength = Ca; this.oApi._fnFeatureHtmlProcessing = Ea; this.oApi._fnProcessingDisplay = K; this.oApi._fnVisibleToColumnIndex = Ja; this.oApi._fnColumnIndexToVisible =
pa;
this.oApi._fnNodeToDataIndex = U;
this.oApi._fnVisbleColumns = X; this.oApi._fnCalculateEnd = E; this.oApi._fnConvertToWidth = Oa; this.oApi._fnCalculateColumnWidths = ea; this.oApi._fnScrollingWidthAdjust = Qa; this.oApi._fnGetWidestNode = Pa; this.oApi._fnGetMaxLenString = Ra; this.oApi._fnStringToCss = u; this.oApi._fnArrayCmp = Va; this.oApi._fnDetectType = fa; this.oApi._fnSettingsFromNode = A; this.oApi._fnGetDataMaster = aa; this.oApi._fnGetTrNodes = $; this.oApi._fnGetTdNodes = Q; this.oApi._fnEscapeRegex = oa; this.oApi._fnDeleteIndex =
ra;
this.oApi._fnReOrderIndex = Ba;
this.oApi._fnColumnOrdering = ha;
this.oApi._fnLog = J;
this.oApi._fnClearTable = ia;
this.oApi._fnSaveState = sa;
this.oApi._fnLoadState = Ta;
this.oApi._fnCreateCookie = Sa;
this.oApi._fnReadCookie = ta;
this.oApi._fnDetectHeader = W; this.oApi._fnGetUniqueThs = S; this.oApi._fnScrollBarWidth = Ua; this.oApi._fnApplyToChildren = P; this.oApi._fnMap = n; this.oApi._fnGetRowData = da; this.oApi._fnGetCellData = H; this.oApi._fnSetCellData = N; this.oApi._fnGetObjectDataFn = Z; this.oApi._fnSetObjectDataFn = ya; var va =
this;
return this.each(function () {
var a = 0, b, c, d, f; a = 0; for (b = D.length; a < b; a++) {
if (D[a].nTable == this) if (typeof g == "undefined" || typeof g.bRetrieve != "undefined" && g.bRetrieve === true) return D[a].oInstance; else if (typeof g.bDestroy != "undefined" && g.bDestroy === true) { D[a].oInstance.fnDestroy(); break } else {
J(D[a], 0, "Cannot reinitialise DataTable.\n\nTo retrieve the DataTables object for this table, please pass either no arguments to the dataTable() function, or set bRetrieve to true. Alternatively, to destory the old table and create a new one, set bDestroy to true (note that a lot of changes to the configuration can be made through the API which is usually much faster).");
return
} if (D[a].sTableId !== "" && D[a].sTableId == this.getAttribute("id")) { D.splice(a, 1); break }
} var e = new l; D.push(e); var h = false, j = false; a = this.getAttribute("id"); if (a !== null) { e.sTableId = a; e.sInstance = a } else e.sInstance = o._oExternConfig.iNextUnique++; if (this.nodeName.toLowerCase() != "table") J(e, 0, "Attempted to initialise DataTables on a node which is not a table: " + this.nodeName); else {
e.nTable = this; e.oInstance = va.length == 1 ? va : i(this).dataTable(); e.oApi = va.oApi; e.sDestroyWidth = i(this).width(); if (typeof g !=
"undefined" && g !== null) {
e.oInit = g; n(e.oFeatures, g, "bPaginate");
n(e.oFeatures, g, "bLengthChange");
n(e.oFeatures, g, "bFilter");
n(e.oFeatures, g, "bSort");
n(e.oFeatures, g, "bInfo");
n(e.oFeatures, g, "bProcessing");
n(e.oFeatures, g, "bAutoWidth");
n(e.oFeatures, g, "bSortClasses");
n(e.oFeatures, g, "bServerSide");
n(e.oFeatures, g, "bDeferRender");
n(e.oScroll, g, "sScrollX", "sX");
n(e.oScroll, g, "sScrollXInner", "sXInner");
n(e.oScroll, g, "sScrollY", "sY");
n(e.oScroll, g, "bScrollCollapse", "bCollapse");
n(e.oScroll, g, "bScrollInfinite", "bInfinite");
n(e.oScroll, g, "iScrollLoadGap", "iLoadGap");
n(e.oScroll, g, "bScrollAutoCss", "bAutoCss");
n(e, g, "asStripClasses");
n(e, g, "fnPreDrawCallback");
n(e, g, "fnRowCallback"); n(e, g, "fnHeaderCallback"); n(e, g, "fnFooterCallback"); n(e, g, "fnCookieCallback"); n(e, g, "fnInitComplete"); n(e, g, "fnServerData"); n(e, g, "fnFormatNumber"); n(e, g, "aaSorting"); n(e, g, "aaSortingFixed"); n(e, g, "aLengthMenu"); n(e, g, "sPaginationType"); n(e, g, "sAjaxSource"); n(e, g, "sAjaxDataProp"); n(e, g, "iCookieDuration"); n(e, g, "sCookiePrefix");
n(e, g, "sDom");
n(e, g, "bSortCellsTop");
n(e, g, "oSearch", "oPreviousSearch");
n(e, g, "aoSearchCols", "aoPreSearchCols");
n(e, g, "iDisplayLength", "_iDisplayLength");
n(e, g, "bJQueryUI", "bJUI");
n(e.oLanguage, g, "fnInfoCallback");
typeof g.fnDrawCallback == "function" && e.aoDrawCallback.push({
fn: g.fnDrawCallback, sName: "user"
});
typeof g.fnStateSaveCallback == "function" && e.aoStateSave.push({
fn: g.fnStateSaveCallback, sName: "user"
});
typeof g.fnStateLoadCallback == "function" && e.aoStateLoad.push({
fn: g.fnStateLoadCallback, sName: "user"
});
if (e.oFeatures.bServerSide && e.oFeatures.bSort && e.oFeatures.bSortClasses) e.aoDrawCallback.push({ fn: T, sName: "server_side_sort_classes" }); else e.oFeatures.bDeferRender && e.aoDrawCallback.push({ fn: T, sName: "defer_sort_classes" }); if (typeof g.bJQueryUI != "undefined" && g.bJQueryUI) { e.oClasses = o.oJUIClasses; if (typeof g.sDom == "undefined") e.sDom = '<"H"lfr>t<"F"ip>' } if (e.oScroll.sX !== "" || e.oScroll.sY !== "") e.oScroll.iBarWidth = Ua(); if (typeof g.iDisplayStart != "undefined" && typeof e.iInitDisplayStart == "undefined") {
e.iInitDisplayStart = g.iDisplayStart; e._iDisplayStart = g.iDisplayStart
} if (typeof g.bStateSave != "undefined") { e.oFeatures.bStateSave = g.bStateSave; Ta(e, g); e.aoDrawCallback.push({ fn: sa, sName: "state_save" }) } if (typeof g.iDeferLoading != "undefined") { e.bDeferLoading = true; e._iRecordsTotal = g.iDeferLoading; e._iRecordsDisplay = g.iDeferLoading } if (typeof g.aaData != "undefined") j = true; if (typeof g != "undefined" && typeof g.aoData != "undefined") g.aoColumns = g.aoData; if (typeof g.oLanguage != "undefined") if (typeof g.oLanguage.sUrl != "undefined" && g.oLanguage.sUrl !==
"") { e.oLanguage.sUrl = g.oLanguage.sUrl; i.getJSON(e.oLanguage.sUrl, null, function (t) { y(e, t, true) }); h = true } else y(e, g.oLanguage, false)
} else g = {}; if (typeof g.asStripClasses == "undefined") { e.asStripClasses.push(e.oClasses.sStripOdd); e.asStripClasses.push(e.oClasses.sStripEven) } c = false; d = i(">tbody>tr", this); a = 0; for (b = e.asStripClasses.length; a < b; a++) if (d.filter(":lt(2)").hasClass(e.asStripClasses[a])) { c = true; break } if (c) {
e.asDestoryStrips = ["", ""]; if (i(d[0]).hasClass(e.oClasses.sStripOdd)) e.asDestoryStrips[0] +=
e.oClasses.sStripOdd + " "; if (i(d[0]).hasClass(e.oClasses.sStripEven)) e.asDestoryStrips[0] += e.oClasses.sStripEven; if (i(d[1]).hasClass(e.oClasses.sStripOdd)) e.asDestoryStrips[1] += e.oClasses.sStripOdd + " "; if (i(d[1]).hasClass(e.oClasses.sStripEven)) e.asDestoryStrips[1] += e.oClasses.sStripEven; d.removeClass(e.asStripClasses.join(" "))
} c = []; var k; a = this.getElementsByTagName("thead"); if (a.length !== 0) { W(e.aoHeader, a[0]); c = S(e) } if (typeof g.aoColumns == "undefined") { k = []; a = 0; for (b = c.length; a < b; a++) k.push(null) } else k =
g.aoColumns; a = 0; for (b = k.length; a < b; a++) { if (typeof g.saved_aoColumns != "undefined" && g.saved_aoColumns.length == b) { if (k[a] === null) k[a] = {}; k[a].bVisible = g.saved_aoColumns[a].bVisible } G(e, c ? c[a] : null) } if (typeof g.aoColumnDefs != "undefined") for (a = g.aoColumnDefs.length - 1; a >= 0; a--) {
var m = g.aoColumnDefs[a].aTargets; i.isArray(m) || J(e, 1, "aTargets must be an array of targets, not a " + typeof m); c = 0; for (d = m.length; c < d; c++) if (typeof m[c] == "number" && m[c] >= 0) { for (; e.aoColumns.length <= m[c]; ) G(e); x(e, m[c], g.aoColumnDefs[a]) } else if (typeof m[c] ==
"number" && m[c] < 0) x(e, e.aoColumns.length + m[c], g.aoColumnDefs[a]); else if (typeof m[c] == "string") { b = 0; for (f = e.aoColumns.length; b < f; b++) if (m[c] == "_all" || i(e.aoColumns[b].nTh).hasClass(m[c])) x(e, b, g.aoColumnDefs[a]) }
} if (typeof k != "undefined") { a = 0; for (b = k.length; a < b; a++) x(e, a, k[a]) } a = 0; for (b = e.aaSorting.length; a < b; a++) {
if (e.aaSorting[a][0] >= e.aoColumns.length)
e.aaSorting[a][0] = 0; k = e.aoColumns[e.aaSorting[a][0]];
if (typeof e.aaSorting[a][2] == "undefined") e.aaSorting[a][2] = 0;
if (typeof g.aaSorting == "undefined" && typeof e.saved_aaSorting == "undefined")
e.aaSorting[a][1] = k.asSorting[0];
c = 0; for (d = k.asSorting.length;
c < d; c++) if (e.aaSorting[a][1] == k.asSorting[c]) {
e.aaSorting[a][2] = c; break
}
} T(e); a = i(">thead", this); if (a.length === 0) {
a = [p.createElement("thead")]; this.appendChild(a[0])
} e.nTHead = a[0]; a = i(">tbody", this);
if (a.length === 0) {
a = [p.createElement("tbody")];
this.appendChild(a[0])
} e.nTBody = a[0]; a = i(">tfoot", this);
if (a.length > 0) { e.nTFoot = a[0]; W(e.aoFooter, e.nTFoot) }
if (j)
for (a = 0; a < g.aaData.length; a++) v(e, g.aaData[a]);
else Y(e); e.aiDisplay = e.aiDisplayMaster.slice(); e.bInitialised = true; h === false && s(e)
}
})
}
})(jQuery, window, document);
File diff suppressed because one or more lines are too long
-41
View File
@@ -1,41 +0,0 @@
window.log = function(){
log.history = log.history || [];
log.history.push(arguments);
arguments.callee = arguments.callee.caller;
if(this.console) console.log( Array.prototype.slice.call(arguments) );
};
(function(b){function c(){}for(var d="assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","),a;a=d.pop();)b[a]=b[a]||c})(window.console=window.console||{});
jQuery.fn.dataTableExt.oSort['title-string-asc'] = function(a,b) {
var x = a.match(/title="(.*?)"/)[1].toLowerCase();
var y = b.match(/title="(.*?)"/)[1].toLowerCase();
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
};
jQuery.fn.dataTableExt.oSort['title-string-desc'] = function(a,b) {
var x = a.match(/title="(.*?)"/)[1].toLowerCase();
var y = b.match(/title="(.*?)"/)[1].toLowerCase();
return ((x < y) ? 1 : ((x > y) ? -1 : 0));
};
jQuery.fn.dataTableExt.oSort['title-numeric-asc'] = function(a,b) {
var x = a.match(/title="*(-?[0-9]+)/)[1];
var y = b.match(/title="*(-?[0-9]+)/)[1];
x = parseFloat( x );
y = parseFloat( y );
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
};
jQuery.fn.dataTableExt.oSort['title-numeric-desc'] = function(a,b) {
var x = a.match(/title="*(-?[0-9]+)/)[1];
var y = b.match(/title="*(-?[0-9]+)/)[1];
x = parseFloat( x );
y = parseFloat( y );
return ((x < y) ? 1 : ((x > y) ? -1 : 0));
};
function toggle(source) {
checkboxes = document.getElementsByClassName('checkbox');
for(var i in checkboxes)
checkboxes[i].checked = source.checked;
}
+43 -19
View File
@@ -67,37 +67,49 @@ function getInfo(elem,id,type) {
}); });
} }
function getImageLinks(elem,id,type) { function getImageLinks(elem,id,type,unveil) {
if ( type == 'artist' ) { if ( type == 'artist' ) {
var infoURL = "getImageLinks?ArtistID=" + id; var infoURL = "getImageLinks?ArtistID=" + id;
} else { } else {
var infoURL = "getImageLinks?AlbumID=" + id; var infoURL = "getImageLinks?AlbumID=" + id;
} }
// Get Data from the cache by ID
// Get Data from the cache by ID
$.ajax({ $.ajax({
url: infoURL, url: infoURL,
cache: true, cache: true,
dataType: "json", dataType: "json",
success: function(data){ success: function(data){
if ( data.thumbnail == "" || data.thumbnail == undefined ) { if (!data) {
// Invalid response
return;
}
if (!data.thumbnail) {
var thumbnail = "interfaces/default/images/no-cover-artist.png"; var thumbnail = "interfaces/default/images/no-cover-artist.png";
} }
else { else {
var thumbnail = data.thumbnail; var thumbnail = data.thumbnail;
} }
if ( data.artwork == "" || data.artwork == undefined ) { if (!data.artwork) {
var artwork = "interfaces/default/images/no-cover-artist.png"; var artwork = "interfaces/default/images/no-cover-artist.png";
} }
else { else {
var artwork = data.artwork; var artwork = data.artwork;
} }
$(elem).attr("src", thumbnail);
if (unveil) {
$(elem).attr("data-src", thumbnail);
$(elem).unveil();
}
else {
$(elem).attr("src", thumbnail);
}
} }
}); });
} }
function initHeader() { function initHeader() {
//settings //settings
var header = $("#container header"); var header = $("#container header");
var fadeSpeed = 100, fadeTo = 0.5, topDistance = 20; var fadeSpeed = 100, fadeTo = 0.5, topDistance = 20;
@@ -228,6 +240,21 @@ function showMsg(msg,loader,timeout,ms) {
} }
} }
function showArtistMsg(msg) {
var feedback = $("#ajaxMsg2");
update = $("#updatebar");
if ( update.is(":visible") ) {
var height = update.height() + 35;
feedback.css("bottom",height + "px");
} else {
feedback.removeAttr("style");
}
feedback.fadeIn();
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
feedback.css("padding","14px 10px")
$(feedback).prepend(message);
}
function doAjaxCall(url,elem,reload,form) { function doAjaxCall(url,elem,reload,form) {
// Set Message // Set Message
feedback = $("#ajaxMsg"); feedback = $("#ajaxMsg");
@@ -316,6 +343,10 @@ function doAjaxCall(url,elem,reload,form) {
} }
if ( reload == "tabs") refreshTab(); if ( reload == "tabs") refreshTab();
if ( reload == "page") location.reload(); if ( reload == "page") location.reload();
if ( reload == "submenu&table") {
refreshSubmenu();
refreshTable();
}
if ( form ) { if ( form ) {
// Change the option to 'choose...' // Change the option to 'choose...'
$(formID + " select").children('option[disabled=disabled]').attr('selected','selected'); $(formID + " select").children('option[disabled=disabled]').attr('selected','selected');
@@ -330,18 +361,16 @@ function doAjaxCall(url,elem,reload,form) {
}); });
} }
function doSimpleAjaxCall(url) {
$.ajax(url);
}
function resetFilters(text){ function resetFilters(text){
if ( $(".dataTables_filter").length > 0 ) { if ( $(".dataTables_filter").length > 0 ) {
$(".dataTables_filter input").attr("placeholder","filter " + text + ""); $(".dataTables_filter input").attr("placeholder","filter " + text + "");
} }
} }
function preventDefault(){
$("a[href='#']").live('click', function(){
return false;
});
}
function initFancybox() { function initFancybox() {
if ( $("a[rel=dialog]").length > 0 ) { if ( $("a[rel=dialog]").length > 0 ) {
$.getScript('interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js', function() { $.getScript('interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js', function() {
@@ -351,11 +380,6 @@ function initFancybox() {
} }
} }
function init() {
initHeader();
preventDefault();
}
$(document).ready(function(){ $(document).ready(function(){
init(); initHeader();
}); });
+20 -8
View File
@@ -1,10 +1,18 @@
lossless<%inherit file="base.html"/> <%inherit file="base.html"/>
<%! <%!
from headphones import helpers from headphones import helpers
%> %>
<%def name="headerIncludes()">
<div id="subhead_container">
<div id="subhead_menu">
<a class="menu_link_edit" href="clearLogs"><i class="fa fa-trash-o"></i> Clear log</a>
</div>
</div>
</%def>
<%def name="body()"> <%def name="body()">
<div class="title"> <div id="paddingheader">
<h1 class="clearfix"><i class="fa fa-flag"></i> Logs</h1> <h1 class="clearfix"><i class="fa fa-flag"></i> Logs</h1>
</div> </div>
<table class="display" id="log_table"> <table class="display" id="log_table">
@@ -19,7 +27,7 @@ lossless<%inherit file="base.html"/>
</tbody> </tbody>
</table> </table>
<br> <br>
<div align="center">Refresh rate: <div align="center">Refresh rate:
<select id="refreshrate" onchange="setRefresh()"> <select id="refreshrate" onchange="setRefresh()">
<option value="0" selected="selected">No Refresh</option> <option value="0" selected="selected">No Refresh</option>
<option value="5">5 Seconds</option> <option value="5">5 Seconds</option>
@@ -29,7 +37,6 @@ lossless<%inherit file="base.html"/>
<option value="300">5 Minutes</option> <option value="300">5 Minutes</option>
<option value="600">10 Minutes</option> <option value="600">10 Minutes</option>
</select></div> </select></div>
</%def> </%def>
<%def name="headIncludes()"> <%def name="headIncludes()">
@@ -40,6 +47,8 @@ lossless<%inherit file="base.html"/>
<script src="js/libs/jquery.dataTables.min.js"></script> <script src="js/libs/jquery.dataTables.min.js"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
initActions();
$('#log_table').dataTable( { $('#log_table').dataTable( {
"bProcessing": true, "bProcessing": true,
"bServerSide": true, "bServerSide": true,
@@ -49,23 +58,26 @@ $(document).ready(function() {
"iDisplayLength": 25, "iDisplayLength": 25,
"bStateSave": true, "bStateSave": true,
"oLanguage": { "oLanguage": {
"sSearch":"", "sSearch":"Filter:",
"sLengthMenu":"Show _MENU_ lines per page", "sLengthMenu":"Show _MENU_ lines per page",
"sEmptyTable": "No log information available", "sEmptyTable": "No log information available",
"sInfo":"Showing _START_ to _END_ of _TOTAL_ lines", "sInfo":"Showing _START_ to _END_ of _TOTAL_ lines",
"sInfoEmpty":"Showing 0 to 0 of 0 lines", "sInfoEmpty":"Showing 0 to 0 of 0 lines",
"sInfoFiltered":"(filtered from _MAX_ total lines)"}, "sInfoFiltered":"(filtered from _MAX_ total lines)"},
"fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) { "fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
if (aData[1] === "WARNING" || aData[1] === "ERROR") if (aData[1] === "ERROR")
{ {
$('td', nRow).closest('tr').addClass("gradeX"); $('td', nRow).closest('tr').addClass("gradeX");
} }
else if (aData[1] === "WARNING")
{
$('td', nRow).closest('tr').addClass("gradeW");
}
else else
{ {
$('td', nRow).closest('tr').addClass("gradeZ"); $('td', nRow).closest('tr').addClass("gradeZ");
} }
return nRow; return nRow;
}, },
"fnServerData": function ( sSource, aoData, fnCallback ) { "fnServerData": function ( sSource, aoData, fnCallback ) {
+1
View File
@@ -14,6 +14,7 @@
<a href="manageAlbums?Status=Snatched"><i class="fa fa-cloud-download fa-fw"></i> </span>Manage Snatched Albums</a><br> <a href="manageAlbums?Status=Snatched"><i class="fa fa-cloud-download fa-fw"></i> </span>Manage Snatched Albums</a><br>
<a href="manageAlbums?Status=Upcoming"><i class="fa fa-calendar fa-fw"></i> Manage Upcoming Albums</a><br> <a href="manageAlbums?Status=Upcoming"><i class="fa fa-calendar fa-fw"></i> Manage Upcoming Albums</a><br>
<a href="manageAlbums?Status=Wanted"><i class="fa fa-heart fa-fw"></i> </span>Manage Wanted Albums</a><br> <a href="manageAlbums?Status=Wanted"><i class="fa fa-heart fa-fw"></i> </span>Manage Wanted Albums</a><br>
<a href="manageAlbums?Status=Ignored"><i class="fa fa-meh-o fa-fw"></i> </span>Manage Ignored Albums</a><br>
<br><br> <br><br>
<a href="manageAlbums">Manage All Albums</a> <a href="manageAlbums">Manage All Albums</a>
</div> </div>
@@ -25,6 +25,7 @@
<option value="WantedNew">Wanted (new only)</option> <option value="WantedNew">Wanted (new only)</option>
<option value="WantedLossless">Wanted (lossless)</option> <option value="WantedLossless">Wanted (lossless)</option>
<option value="Skipped">Skipped</option> <option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option> <option value="Downloaded">Downloaded</option>
</select> </select>
<input type="hidden" value="Go"> <input type="hidden" value="Go">
@@ -50,6 +51,8 @@
grade = 'Z' grade = 'Z'
elif album['Status'] == 'Wanted': elif album['Status'] == 'Wanted':
grade = 'X' grade = 'X'
elif album['Status'] == 'Ignored':
grade = 'I'
elif album['Status'] == 'Snatched': elif album['Status'] == 'Snatched':
grade = 'C' grade = 'C'
else: else:
+61 -46
View File
@@ -11,36 +11,49 @@
<th id="albumart"></th> <th id="albumart"></th>
%if type == 'album': %if type == 'album':
<th id="albumname">Album Name</th> <th id="albumname">Album Name</th>
<th id="artistnamesmall">Artist Name</th>
<th id="format">Format</th>
<th id="tracks">Tracks</th>
<th id="reldate">Date</th>
<th id="scoresmall">Score</th>
%else:
<th id="artistname">Artist Name</th>
<th id="score">Score</th>
%endif %endif
<th id="artistname">Artist Name</th> <th id="mb"></th>
<th id="score">Score</th>
<th id="add"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
%if searchresults: %if searchresults:
%for result in searchresults: %for result in searchresults:
<% <%
if result['score'] == 100: if result['score'] == 100:
grade = 'A' grade = 'A'
else: else:
grade = 'Z' grade = 'Z'
%>
if type == 'album':
albuminfo = 'Type: ' + result['rgtype'] + ', Country: ' + result['country']
caa_group_url = "http://coverartarchive.org/release-group/%s/front-250.jpg" %result['rgid']
%>
<tr class="grade${grade}"> <tr class="grade${grade}">
%if type == 'album': %if type == 'album':
<td id="albumart"><div id="artistImg"><img title="${result['albumid']}" class="albumArt" height="50" width="50"></div></td> <td id="albumart" style=" text-align: center; vertical-align: middle;"><div id="artistImg"><img title="${result['albumid']}" class="albumArt" height="50" width="50" onerror="this.onerror=null;this.src='${caa_group_url}'"></div></td>
%else: %else:
<td id="albumart"><div id="artistImg"><img title="${result['id']}" class="albumArt" height="50" width="50"></div></td> <td id="albumart"><div id="artistImg"><img title="${result['id']}" class="albumArt" height="50" width="50"></div></td>
%endif %endif
%if type == 'album': %if type == 'album':
<td id="albumname"><a href="${result['albumurl']}">${result['title']}</a></td> <td id="albumname"><a href="addReleaseById?rid=${result['albumid']}&rgid=${result['rgid']}" title="${albuminfo}">${result['title']}</a></td>
%endif <td id="artistnamesmall"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
<td id="artistname"><a href="${result['url']}" title="${result['uniquename']}">${result['uniquename']}</a></td> <td id="format">${result['formats']}</td>
<td id="score"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></td> <td id="tracks">${result['tracks']}</td>
%if type == 'album': <td id="reldate">${result['date']}</td>
<td id="add"><a href="addReleaseById?rid=${result['albumid']}"><i class="fa fa-plus"></i> Add this album</a></td> <td id="score"><a href="${result['albumurl']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
%else: <td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['albumurl']}"><img src="interfaces/default/images/MusicBrainz_Album_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td>
<td id="add"><a href="addArtist?artistid=${result['id']}"><i class="fa fa-plus"></i> Add this artist</a></td> %else:
<td id="artistname"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
<td id="score"><a href="${result['url']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
<td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['url']}"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td>
%endif %endif
</tr> </tr>
%endfor %endfor
@@ -55,28 +68,29 @@
</%def> </%def>
<%def name="javascriptIncludes()"> <%def name="javascriptIncludes()">
<script src="js/libs/jquery.unveil.min.js"></script>
<script src="js/libs/jquery.dataTables.min.js"></script> <script src="js/libs/jquery.dataTables.min.js"></script>
<script> <script type="text/javascript">
function getArt() {
$("table#searchresults_table tr td#albumart img").each(function(){ function getArt() {
var id = $(this).attr('title'); $("table#searchresults_table tr td#albumart img").each(function(){
var image = $(this); var id = $(this).attr('title');
if ( !image.hasClass('done') ) { var image = $(this);
image.addClass('done');
getImageLinks(image,id,"${type}"); if (!image.hasClass('done')) {
} image.addClass('done');
}); getImageLinks(image, id, "${type}", true);
} }
function initThisPage() { });
getArt(); }
$('#searchresults_table').dataTable( function initThisPage() {
{ getArt();
$('#searchresults_table').dataTable({
"bDestroy": true, "bDestroy": true,
"aoColumnDefs": [ "aoColumnDefs": [
{ 'bSortable': false, 'aTargets': [ 0,3 ] } { 'bSortable': false, 'aTargets': [ 0 ] }
], ],
"oLanguage": { "oLanguage": {
"sLengthMenu":"Show _MENU_ results per page", "sLengthMenu":"Show _MENU_ results per page",
"sEmptyTable": "No results", "sEmptyTable": "No results",
@@ -88,13 +102,14 @@
"sPaginationType": "full_numbers", "sPaginationType": "full_numbers",
"aaSorting": [] "aaSorting": []
}); });
$('#searchresults_table').on("draw.dt", function () {
$("img").unveil();
});
resetFilters("album"); resetFilters("album");
} }
$(document).ready(function(){ $(document).ready(function(){
initThisPage(); initFancybox();
}); initThisPage();
$(window).load(function(){ });
initFancybox();
});
</script> </script>
</%def> </%def>
+24 -23
View File
@@ -5,21 +5,21 @@
<div id="subhead_menu"> <div id="subhead_menu">
<a href="#" id="menu_link_scan" onclick="doAjaxCall('forceSearch',$(this))" data-success="Checking for wanted albums successful" data-error="Error checking wanted albums"><i class="fa fa-search"></i> Force Check</a> <a href="#" id="menu_link_scan" onclick="doAjaxCall('forceSearch',$(this))" data-success="Checking for wanted albums successful" data-error="Error checking wanted albums"><i class="fa fa-search"></i> Force Check</a>
</div> </div>
</div> </div>
</%def> </%def>
<%def name="body()"> <%def name="body()">
<div id="paddingheader">
<div class="title">
<h1 class="clearfix"><i class="fa fa-heart"></i> Wanted Albums</h1> <h1 class="clearfix"><i class="fa fa-heart"></i> Wanted Albums</h1>
</div> </div>
<form action="markAlbums" method="get" id="markAlbums"> <form action="markAlbums" method="get" id="markAlbums">
<div id="markalbum" style="top:0;"> <div id="markalbum" style="top:0;">
Mark selected albums as Mark selected albums as
<select name="action" onChange="doAjaxCall('markAlbums',$(this),'table',true);" data-error="You didn't select any albums"> <select name="action" onChange="doAjaxCall('markAlbums',$(this),'table',true);" data-error="You didn't select any albums">
<option disabled="disabled" selected="selected">Choose...</option> <option disabled="disabled" selected="selected">Choose...</option>
<option value="Skipped">Skipped</option> <option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option> <option value="Downloaded">Downloaded</option>
</select> </select>
<input type="hidden" value="Go"> <input type="hidden" value="Go">
@@ -40,7 +40,7 @@
%for album in wanted: %for album in wanted:
<tr class="gradeZ"> <tr class="gradeZ">
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></th> <td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></th>
<td id="albumart"><img title="${album['AlbumID']}" height="64" width="64" src="artwork/thumbs/album/${album['AlbumID']}"></td> <td id="albumart"><img title="${album['AlbumID']}" height="64" width="64" src="interfaces/default/images/no-cover-art.png" data-src="artwork/thumbs/album/${album['AlbumID']}"></td>
<td id="artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td> <td id="artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td> <td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
<td id="reldate">${album['ReleaseDate']}</td> <td id="reldate">${album['ReleaseDate']}</td>
@@ -49,7 +49,7 @@
%endfor %endfor
</tbody> </tbody>
</table> </table>
</form> </form>
</div> </div>
<div class="title"> <div class="title">
@@ -70,7 +70,7 @@
<tbody> <tbody>
%for album in upcoming: %for album in upcoming:
<tr class="gradeZ"> <tr class="gradeZ">
<td id="albumart"><img title="${album['AlbumID']}" height="64" width="64" src="artwork/thumbs/album/${album['AlbumID']}"></td> <td id="albumart"><img title="${album['AlbumID']}" height="64" width="64" data-src="artwork/thumbs/album/${album['AlbumID']}"></td>
<td id="artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td> <td id="artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td> <td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
<td id="reldate">${album['ReleaseDate']}</td> <td id="reldate">${album['ReleaseDate']}</td>
@@ -88,24 +88,25 @@
</%def> </%def>
<%def name="javascriptIncludes()"> <%def name="javascriptIncludes()">
<script src="js/libs/jquery.unveil.min.js"></script>
<script src="js/libs/jquery.dataTables.min.js"></script> <script src="js/libs/jquery.dataTables.min.js"></script>
<script> <script>
function initThisPage() { function initThisPage() {
$("img").unveil();
$('#wanted_table').dataTable({ $('#wanted_table').dataTable({
"oLanguage": { "oLanguage": {
"sEmptyTable": " " "sEmptyTable": " "
}, },
"bDestroy":true, "bDestroy":true,
"bFilter": false, "bFilter": false,
"bInfo": false, "bInfo": false,
"bPaginate": false "bPaginate": false
});
resetFilters("artists");
initActions();
}
$(document).ready(function() {
initThisPage();
}); });
resetFilters("artists");
initActions();
}
$(document).ready(function() {
initThisPage();
});
</script> </script>
</%def> </%def>
+17
View File
@@ -0,0 +1,17 @@
window.log = function(){
log.history = log.history || [];
log.history.push(arguments);
arguments.callee = arguments.callee.caller;
if (this.console) {
console.log(Array.prototype.slice.call(arguments));
}
};
function toggle(source) {
checkboxes = document.getElementsByClassName('checkbox');
for (var i in checkboxes) {
checkboxes[i].checked = source.checked;
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-8981
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored Regular → Executable
+13 -121
View File
File diff suppressed because one or more lines are too long
-2
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
!function(t){t.fn.unveil=function(i,e){function n(){var i=a.filter(function(){var i=t(this);if(!i.is(":hidden")){var e=o.scrollTop(),n=e+o.height(),r=i.offset().top,s=r+i.height();return s>=e-u&&n+u>=r}});r=i.trigger("unveil"),a=a.not(r)}var r,o=t(window),u=i||0,s=window.devicePixelRatio>1,l=s?"data-src-retina":"data-src",a=this;return this.one("unveil",function(){var t=this.getAttribute(l);t=t||this.getAttribute("data-src"),t&&(this.setAttribute("src",t),"function"==typeof e&&e.call(this))}),o.on("scroll.unveil resize.unveil lookup.unveil",n),n(),this}}(window.jQuery||window.Zepto);
File diff suppressed because one or more lines are too long
+4
View File
File diff suppressed because one or more lines are too long
-41
View File
@@ -1,41 +0,0 @@
window.log = function(){
log.history = log.history || [];
log.history.push(arguments);
arguments.callee = arguments.callee.caller;
if(this.console) console.log( Array.prototype.slice.call(arguments) );
};
(function(b){function c(){}for(var d="assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","),a;a=d.pop();)b[a]=b[a]||c})(window.console=window.console||{});
jQuery.fn.dataTableExt.oSort['title-string-asc'] = function(a,b) {
var x = a.match(/title="(.*?)"/)[1].toLowerCase();
var y = b.match(/title="(.*?)"/)[1].toLowerCase();
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
};
jQuery.fn.dataTableExt.oSort['title-string-desc'] = function(a,b) {
var x = a.match(/title="(.*?)"/)[1].toLowerCase();
var y = b.match(/title="(.*?)"/)[1].toLowerCase();
return ((x < y) ? 1 : ((x > y) ? -1 : 0));
};
jQuery.fn.dataTableExt.oSort['title-numeric-asc'] = function(a,b) {
var x = a.match(/title="*(-?[0-9]+)/)[1];
var y = b.match(/title="*(-?[0-9]+)/)[1];
x = parseFloat( x );
y = parseFloat( y );
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
};
jQuery.fn.dataTableExt.oSort['title-numeric-desc'] = function(a,b) {
var x = a.match(/title="*(-?[0-9]+)/)[1];
var y = b.match(/title="*(-?[0-9]+)/)[1];
x = parseFloat( x );
y = parseFloat( y );
return ((x < y) ? 1 : ((x > y) ? -1 : 0));
};
function toggle(source) {
checkboxes = document.getElementsByClassName('checkbox');
for(var i in checkboxes)
checkboxes[i].checked = source.checked;
}
+159 -85
View File
@@ -15,20 +15,18 @@
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi # NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
from __future__ import with_statement import os
import sys
import os, sys, subprocess import subprocess
import threading import threading
import webbrowser import webbrowser
import sqlite3 import sqlite3
import itertools import itertools
from lib.apscheduler.scheduler import Scheduler
from lib.configobj import ConfigObj
import cherrypy import cherrypy
from apscheduler.scheduler import Scheduler
from configobj import ConfigObj
from headphones import versioncheck, logger, version from headphones import versioncheck, logger, version
from headphones.common import * from headphones.common import *
@@ -41,7 +39,8 @@ SIGNAL = None
SYS_PLATFORM = None SYS_PLATFORM = None
SYS_ENCODING = None SYS_ENCODING = None
VERBOSE = 1 QUIET = False
VERBOSE = False
DAEMON = False DAEMON = False
CREATEPID = False CREATEPID = False
PIDFILE= None PIDFILE= None
@@ -106,11 +105,15 @@ PREFERRED_BITRATE_HIGH_BUFFER = None
PREFERRED_BITRATE_LOW_BUFFER = None PREFERRED_BITRATE_LOW_BUFFER = None
PREFERRED_BITRATE_ALLOW_LOSSLESS = False PREFERRED_BITRATE_ALLOW_LOSSLESS = False
DETECT_BITRATE = False DETECT_BITRATE = False
LOSSLESS_BITRATE_FROM = None
LOSSLESS_BITRATE_TO = None
ADD_ARTISTS = False ADD_ARTISTS = False
CORRECT_METADATA = False CORRECT_METADATA = False
FREEZE_DB = False
MOVE_FILES = False MOVE_FILES = False
RENAME_FILES = False RENAME_FILES = False
CLEANUP_FILES = False CLEANUP_FILES = False
KEEP_NFO = False
ADD_ALBUM_ART = False ADD_ALBUM_ART = False
ALBUM_ART_FORMAT = None ALBUM_ART_FORMAT = None
EMBED_ALBUM_ART = False EMBED_ALBUM_ART = False
@@ -136,6 +139,7 @@ LIBRARYSCAN_INTERVAL = 300
DOWNLOAD_SCAN_INTERVAL = 5 DOWNLOAD_SCAN_INTERVAL = 5
UPDATE_DB_INTERVAL = 24 UPDATE_DB_INTERVAL = 24
MB_IGNORE_AGE = 365 MB_IGNORE_AGE = 365
TORRENT_REMOVAL_INTERVAL = 720
SAB_HOST = None SAB_HOST = None
SAB_USERNAME = None SAB_USERNAME = None
@@ -147,6 +151,7 @@ NZBGET_USERNAME = None
NZBGET_PASSWORD = None NZBGET_PASSWORD = None
NZBGET_CATEGORY = None NZBGET_CATEGORY = None
NZBGET_HOST = None NZBGET_HOST = None
NZBGET_PRIORITY = 0
HEADPHONES_INDEXER = False HEADPHONES_INDEXER = False
@@ -157,6 +162,7 @@ TRANSMISSION_PASSWORD = None
UTORRENT_HOST = None UTORRENT_HOST = None
UTORRENT_USERNAME = None UTORRENT_USERNAME = None
UTORRENT_PASSWORD = None UTORRENT_PASSWORD = None
UTORRENT_LABEL = None
NEWZNAB = False NEWZNAB = False
NEWZNAB_HOST = None NEWZNAB_HOST = None
@@ -168,10 +174,6 @@ NZBSORG = False
NZBSORG_UID = None NZBSORG_UID = None
NZBSORG_HASH = None NZBSORG_HASH = None
NZBSRUS = False
NZBSRUS_UID = None
NZBSRUS_APIKEY = None
OMGWTFNZBS = False OMGWTFNZBS = False
OMGWTFNZBS_UID = None OMGWTFNZBS_UID = None
OMGWTFNZBS_APIKEY = None OMGWTFNZBS_APIKEY = None
@@ -190,20 +192,26 @@ ALBUM_COMPLETION_PCT = None # This is used in importer.py to determine how co
TORRENTBLACKHOLE_DIR = None TORRENTBLACKHOLE_DIR = None
NUMBEROFSEEDERS = 10 NUMBEROFSEEDERS = 10
ISOHUNT = None
KAT = None KAT = None
KAT_PROXY_URL = None
KAT_RATIO = None
MININOVA = None MININOVA = None
MININOVA_RATIO = None
PIRATEBAY = None PIRATEBAY = None
PIRATEBAY_PROXY_URL = None PIRATEBAY_PROXY_URL = None
PIRATEBAY_RATIO = None
WAFFLES = None WAFFLES = None
WAFFLES_UID = None WAFFLES_UID = None
WAFFLES_PASSKEY = None WAFFLES_PASSKEY = None
WAFFLES_RATIO = None
RUTRACKER = None RUTRACKER = None
RUTRACKER_USER = None RUTRACKER_USER = None
RUTRACKER_PASSWORD = None RUTRACKER_PASSWORD = None
RUTRACKER_RATIO = None
WHATCD = None WHATCD = None
WHATCD_USERNAME = None WHATCD_USERNAME = None
WHATCD_PASSWORD = None WHATCD_PASSWORD = None
WHATCD_RATIO = None
DOWNLOAD_TORRENT_DIR = None DOWNLOAD_TORRENT_DIR = None
INTERFACE = None INTERFACE = None
@@ -250,7 +258,7 @@ PLEX_UPDATE = False
PLEX_NOTIFY = False PLEX_NOTIFY = False
NMA_ENABLED = False NMA_ENABLED = False
NMA_APIKEY = None NMA_APIKEY = None
NMA_PRIORITY = None NMA_PRIORITY = 0
NMA_ONSNATCH = None NMA_ONSNATCH = None
PUSHALOT_ENABLED = False PUSHALOT_ENABLED = False
PUSHALOT_APIKEY = None PUSHALOT_APIKEY = None
@@ -270,6 +278,16 @@ TWITTER_ONSNATCH = False
TWITTER_USERNAME = None TWITTER_USERNAME = None
TWITTER_PASSWORD = None TWITTER_PASSWORD = None
TWITTER_PREFIX = None TWITTER_PREFIX = None
OSX_NOTIFY_ENABLED = False
OSX_NOTIFY_ONSNATCH = False
OSX_NOTIFY_APP = None
BOXCAR_ENABLED = False
BOXCAR_ONSNATCH = False
BOXCAR_TOKEN = None
SUBSONIC_ENABLED = False
SUBSONIC_HOST = None
SUBSONIC_USERNAME = None
SUBSONIC_PASSWORD = None
MIRRORLIST = ["musicbrainz.org","headphones","custom"] MIRRORLIST = ["musicbrainz.org","headphones","custom"]
MIRROR = None MIRROR = None
CUSTOMHOST = None CUSTOMHOST = None
@@ -281,12 +299,15 @@ SONGKICK_ENABLED = False
SONGKICK_APIKEY = None SONGKICK_APIKEY = None
SONGKICK_LOCATION = None SONGKICK_LOCATION = None
SONGKICK_FILTER_ENABLED = False SONGKICK_FILTER_ENABLED = False
MPC_ENABLED = False
CACHE_SIZEMB = 32 CACHE_SIZEMB = 32
JOURNAL_MODE = None JOURNAL_MODE = None
UMASK = None UMASK = None
VERIFY_SSL_CERT = True
def CheckSection(sec): def CheckSection(sec):
""" Check if INI section exists, if not create it """ """ Check if INI section exists, if not create it """
try: try:
@@ -333,61 +354,43 @@ def initialize():
with INIT_LOCK: with INIT_LOCK:
global __INITIALIZED__, FULL_PATH, PROG_DIR, VERBOSE, DAEMON, SYS_PLATFORM, DATA_DIR, CONFIG_FILE, CFG, CONFIG_VERSION, LOG_DIR, CACHE_DIR, \ global __INITIALIZED__, FULL_PATH, PROG_DIR, VERBOSE, QUIET, DAEMON, SYS_PLATFORM, DATA_DIR, CONFIG_FILE, CFG, CONFIG_VERSION, LOG_DIR, CACHE_DIR, \
HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, HTTP_PROXY, LAUNCH_BROWSER, API_ENABLED, API_KEY, GIT_PATH, GIT_USER, GIT_BRANCH, DO_NOT_OVERRIDE_GIT_BRANCH, \ HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, HTTP_PROXY, LAUNCH_BROWSER, API_ENABLED, API_KEY, GIT_PATH, GIT_USER, GIT_BRANCH, DO_NOT_OVERRIDE_GIT_BRANCH, \
CURRENT_VERSION, LATEST_VERSION, CHECK_GITHUB, CHECK_GITHUB_ON_STARTUP, CHECK_GITHUB_INTERVAL, MUSIC_DIR, DESTINATION_DIR, \ CURRENT_VERSION, LATEST_VERSION, CHECK_GITHUB, CHECK_GITHUB_ON_STARTUP, CHECK_GITHUB_INTERVAL, MUSIC_DIR, DESTINATION_DIR, \
LOSSLESS_DESTINATION_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, ADD_ARTISTS, CORRECT_METADATA, MOVE_FILES, \ LOSSLESS_DESTINATION_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, ADD_ARTISTS, CORRECT_METADATA, FREEZE_DB, MOVE_FILES, \
RENAME_FILES, FOLDER_FORMAT, FILE_FORMAT, FILE_UNDERSCORES, CLEANUP_FILES, INCLUDE_EXTRAS, EXTRAS, AUTOWANT_UPCOMING, AUTOWANT_ALL, KEEP_TORRENT_FILES, PREFER_TORRENTS, OPEN_MAGNET_LINKS, \ RENAME_FILES, FOLDER_FORMAT, FILE_FORMAT, FILE_UNDERSCORES, CLEANUP_FILES, KEEP_NFO, INCLUDE_EXTRAS, EXTRAS, AUTOWANT_UPCOMING, AUTOWANT_ALL, KEEP_TORRENT_FILES, PREFER_TORRENTS, OPEN_MAGNET_LINKS, \
ADD_ALBUM_ART, ALBUM_ART_FORMAT, EMBED_ALBUM_ART, EMBED_LYRICS, REPLACE_EXISTING_FOLDERS, DOWNLOAD_DIR, BLACKHOLE, BLACKHOLE_DIR, USENET_RETENTION, SEARCH_INTERVAL, \ ADD_ALBUM_ART, ALBUM_ART_FORMAT, EMBED_ALBUM_ART, EMBED_LYRICS, REPLACE_EXISTING_FOLDERS, DOWNLOAD_DIR, BLACKHOLE, BLACKHOLE_DIR, USENET_RETENTION, SEARCH_INTERVAL, \
TORRENTBLACKHOLE_DIR, NUMBEROFSEEDERS, ISOHUNT, KAT, PIRATEBAY, PIRATEBAY_PROXY_URL, MININOVA, WAFFLES, WAFFLES_UID, WAFFLES_PASSKEY, \ TORRENTBLACKHOLE_DIR, NUMBEROFSEEDERS, KAT, KAT_PROXY_URL, KAT_RATIO, PIRATEBAY, PIRATEBAY_PROXY_URL, PIRATEBAY_RATIO, MININOVA, MININOVA_RATIO, WAFFLES, WAFFLES_UID, WAFFLES_PASSKEY, WAFFLES_RATIO, \
RUTRACKER, RUTRACKER_USER, RUTRACKER_PASSWORD, WHATCD, WHATCD_USERNAME, WHATCD_PASSWORD, DOWNLOAD_TORRENT_DIR, \ RUTRACKER, RUTRACKER_USER, RUTRACKER_PASSWORD, RUTRACKER_RATIO, WHATCD, WHATCD_USERNAME, WHATCD_PASSWORD, WHATCD_RATIO, DOWNLOAD_TORRENT_DIR, \
LIBRARYSCAN, LIBRARYSCAN_INTERVAL, DOWNLOAD_SCAN_INTERVAL, UPDATE_DB_INTERVAL, MB_IGNORE_AGE, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, \ LIBRARYSCAN, LIBRARYSCAN_INTERVAL, DOWNLOAD_SCAN_INTERVAL, UPDATE_DB_INTERVAL, MB_IGNORE_AGE, TORRENT_REMOVAL_INTERVAL, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, \
NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_HOST, HEADPHONES_INDEXER, NZBMATRIX, TRANSMISSION_HOST, TRANSMISSION_USERNAME, TRANSMISSION_PASSWORD, \ NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_PRIORITY, NZBGET_HOST, HEADPHONES_INDEXER, NZBMATRIX, TRANSMISSION_HOST, TRANSMISSION_USERNAME, TRANSMISSION_PASSWORD, \
UTORRENT_HOST, UTORRENT_USERNAME, UTORRENT_PASSWORD, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, NEWZNAB_ENABLED, EXTRA_NEWZNABS, \ UTORRENT_HOST, UTORRENT_USERNAME, UTORRENT_PASSWORD, UTORRENT_LABEL, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, NEWZNAB_ENABLED, EXTRA_NEWZNABS, \
NZBSORG, NZBSORG_UID, NZBSORG_HASH, NZBSRUS, NZBSRUS_UID, NZBSRUS_APIKEY, OMGWTFNZBS, OMGWTFNZBS_UID, OMGWTFNZBS_APIKEY, \ NZBSORG, NZBSORG_UID, NZBSORG_HASH, OMGWTFNZBS, OMGWTFNZBS_UID, OMGWTFNZBS_APIKEY, \
NZB_DOWNLOADER, TORRENT_DOWNLOADER, PREFERRED_WORDS, REQUIRED_WORDS, IGNORED_WORDS, LASTFM_USERNAME, \ NZB_DOWNLOADER, TORRENT_DOWNLOADER, PREFERRED_WORDS, REQUIRED_WORDS, IGNORED_WORDS, LASTFM_USERNAME, \
INTERFACE, FOLDER_PERMISSIONS, FILE_PERMISSIONS, ENCODERFOLDER, ENCODER_PATH, ENCODER, XLDPROFILE, BITRATE, SAMPLINGFREQUENCY, \ INTERFACE, FOLDER_PERMISSIONS, FILE_PERMISSIONS, ENCODERFOLDER, ENCODER_PATH, ENCODER, XLDPROFILE, BITRATE, SAMPLINGFREQUENCY, \
MUSIC_ENCODER, ADVANCEDENCODER, ENCODEROUTPUTFORMAT, ENCODERQUALITY, ENCODERVBRCBR, ENCODERLOSSLESS, ENCODER_MULTICORE, ENCODER_MULTICORE_COUNT, DELETE_LOSSLESS_FILES, \ MUSIC_ENCODER, ADVANCEDENCODER, ENCODEROUTPUTFORMAT, ENCODERQUALITY, ENCODERVBRCBR, ENCODERLOSSLESS, ENCODER_MULTICORE, ENCODER_MULTICORE_COUNT, DELETE_LOSSLESS_FILES, \
GROWL_ENABLED, GROWL_HOST, GROWL_PASSWORD, GROWL_ONSNATCH, PROWL_ENABLED, PROWL_PRIORITY, PROWL_KEYS, PROWL_ONSNATCH, PUSHOVER_ENABLED, PUSHOVER_PRIORITY, PUSHOVER_KEYS, PUSHOVER_ONSNATCH, PUSHOVER_APITOKEN, MIRRORLIST, \ GROWL_ENABLED, GROWL_HOST, GROWL_PASSWORD, GROWL_ONSNATCH, PROWL_ENABLED, PROWL_PRIORITY, PROWL_KEYS, PROWL_ONSNATCH, PUSHOVER_ENABLED, PUSHOVER_PRIORITY, PUSHOVER_KEYS, PUSHOVER_ONSNATCH, PUSHOVER_APITOKEN, MIRRORLIST, \
TWITTER_ENABLED, TWITTER_ONSNATCH, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, \ TWITTER_ENABLED, TWITTER_ONSNATCH, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, OSX_NOTIFY_ENABLED, OSX_NOTIFY_ONSNATCH, OSX_NOTIFY_APP, BOXCAR_ENABLED, BOXCAR_ONSNATCH, BOXCAR_TOKEN, \
PUSHBULLET_ENABLED, PUSHBULLET_APIKEY, PUSHBULLET_DEVICEID, PUSHBULLET_ONSNATCH, \ PUSHBULLET_ENABLED, PUSHBULLET_APIKEY, PUSHBULLET_DEVICEID, PUSHBULLET_ONSNATCH, \
MIRROR, CUSTOMHOST, CUSTOMPORT, CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, \ MIRROR, CUSTOMHOST, CUSTOMPORT, CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, \
XBMC_NOTIFY, LMS_ENABLED, LMS_HOST, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \ XBMC_NOTIFY, LMS_ENABLED, LMS_HOST, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, NMA_ONSNATCH, SYNOINDEX_ENABLED, ALBUM_COMPLETION_PCT, PREFERRED_BITRATE_HIGH_BUFFER, \
PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \ PREFERRED_BITRATE_LOW_BUFFER, PREFERRED_BITRATE_ALLOW_LOSSLESS, LOSSLESS_BITRATE_FROM, LOSSLESS_BITRATE_TO, CACHE_SIZEMB, JOURNAL_MODE, UMASK, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \
PLEX_ENABLED, PLEX_SERVER_HOST, PLEX_CLIENT_HOST, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE, PLEX_NOTIFY, PUSHALOT_ENABLED, PUSHALOT_APIKEY, \ PLEX_ENABLED, PLEX_SERVER_HOST, PLEX_CLIENT_HOST, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE, PLEX_NOTIFY, PUSHALOT_ENABLED, PUSHALOT_APIKEY, \
PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED PUSHALOT_ONSNATCH, SONGKICK_ENABLED, SONGKICK_APIKEY, SONGKICK_LOCATION, SONGKICK_FILTER_ENABLED, SUBSONIC_ENABLED, SUBSONIC_HOST, SUBSONIC_USERNAME, SUBSONIC_PASSWORD, VERIFY_SSL_CERT
if __INITIALIZED__: if __INITIALIZED__:
return False return False
# Make sure all the config sections exist # Make sure all the config sections exist
CheckSection('General') for section in ('General', 'SABnzbd', 'NZBget', 'Transmission',
CheckSection('SABnzbd') 'uTorrent', 'Headphones', 'Newznab', 'NZBsorg',
CheckSection('NZBget') 'omgwtfnzbs', 'Piratebay', 'Kat', 'Mininova', 'Waffles',
CheckSection('Transmission') 'Rutracker', 'What.cd', 'Growl', 'Prowl', 'Pushover',
CheckSection('uTorrent') 'PushBullet', 'XBMC', 'LMS', 'Plex', 'NMA', 'Pushalot',
CheckSection('Headphones') 'Synoindex', 'Twitter', 'OSX_Notify', 'Boxcar',
CheckSection('Newznab') 'Songkick', 'Advanced'):
CheckSection('NZBsorg') CheckSection(section)
CheckSection('NZBsRus')
CheckSection('omgwtfnzbs')
CheckSection('Waffles')
CheckSection('Rutracker')
CheckSection('What.cd')
CheckSection('Growl')
CheckSection('Prowl')
CheckSection('Pushover')
CheckSection('PushBullet')
CheckSection('XBMC')
CheckSection('LMS')
CheckSection('Plex')
CheckSection('NMA')
CheckSection('Pushalot')
CheckSection('Synoindex')
CheckSection('Twitter')
CheckSection('Songkick')
CheckSection('Advanced')
# Set global variables based on config file or use defaults # Set global variables based on config file or use defaults
CONFIG_VERSION = check_setting_str(CFG, 'General', 'config_version', '0') CONFIG_VERSION = check_setting_str(CFG, 'General', 'config_version', '0')
@@ -431,14 +434,18 @@ def initialize():
PREFERRED_BITRATE_LOW_BUFFER = check_setting_int(CFG, 'General', 'preferred_bitrate_low_buffer', '') PREFERRED_BITRATE_LOW_BUFFER = check_setting_int(CFG, 'General', 'preferred_bitrate_low_buffer', '')
PREFERRED_BITRATE_ALLOW_LOSSLESS = bool(check_setting_int(CFG, 'General', 'preferred_bitrate_allow_lossless', 0)) PREFERRED_BITRATE_ALLOW_LOSSLESS = bool(check_setting_int(CFG, 'General', 'preferred_bitrate_allow_lossless', 0))
DETECT_BITRATE = bool(check_setting_int(CFG, 'General', 'detect_bitrate', 0)) DETECT_BITRATE = bool(check_setting_int(CFG, 'General', 'detect_bitrate', 0))
LOSSLESS_BITRATE_FROM = check_setting_int(CFG, 'General', 'lossless_bitrate_from', '')
LOSSLESS_BITRATE_TO = check_setting_int(CFG, 'General', 'lossless_bitrate_to', '')
ADD_ARTISTS = bool(check_setting_int(CFG, 'General', 'auto_add_artists', 1)) ADD_ARTISTS = bool(check_setting_int(CFG, 'General', 'auto_add_artists', 1))
CORRECT_METADATA = bool(check_setting_int(CFG, 'General', 'correct_metadata', 0)) CORRECT_METADATA = bool(check_setting_int(CFG, 'General', 'correct_metadata', 0))
FREEZE_DB = bool(check_setting_int(CFG, 'General', 'freeze_db', 0))
MOVE_FILES = bool(check_setting_int(CFG, 'General', 'move_files', 0)) MOVE_FILES = bool(check_setting_int(CFG, 'General', 'move_files', 0))
RENAME_FILES = bool(check_setting_int(CFG, 'General', 'rename_files', 0)) RENAME_FILES = bool(check_setting_int(CFG, 'General', 'rename_files', 0))
FOLDER_FORMAT = check_setting_str(CFG, 'General', 'folder_format', 'Artist/Album [Year]') FOLDER_FORMAT = check_setting_str(CFG, 'General', 'folder_format', 'Artist/Album [Year]')
FILE_FORMAT = check_setting_str(CFG, 'General', 'file_format', 'Track Artist - Album [Year] - Title') FILE_FORMAT = check_setting_str(CFG, 'General', 'file_format', 'Track Artist - Album [Year] - Title')
FILE_UNDERSCORES = bool(check_setting_int(CFG, 'General', 'file_underscores', 0)) FILE_UNDERSCORES = bool(check_setting_int(CFG, 'General', 'file_underscores', 0))
CLEANUP_FILES = bool(check_setting_int(CFG, 'General', 'cleanup_files', 0)) CLEANUP_FILES = bool(check_setting_int(CFG, 'General', 'cleanup_files', 0))
KEEP_NFO = bool(check_setting_int(CFG, 'General', 'keep_nfo', 0))
ADD_ALBUM_ART = bool(check_setting_int(CFG, 'General', 'add_album_art', 0)) ADD_ALBUM_ART = bool(check_setting_int(CFG, 'General', 'add_album_art', 0))
ALBUM_ART_FORMAT = check_setting_str(CFG, 'General', 'album_art_format', 'folder') ALBUM_ART_FORMAT = check_setting_str(CFG, 'General', 'album_art_format', 'folder')
EMBED_ALBUM_ART = bool(check_setting_int(CFG, 'General', 'embed_album_art', 0)) EMBED_ALBUM_ART = bool(check_setting_int(CFG, 'General', 'embed_album_art', 0))
@@ -464,27 +471,37 @@ def initialize():
DOWNLOAD_SCAN_INTERVAL = check_setting_int(CFG, 'General', 'download_scan_interval', 5) DOWNLOAD_SCAN_INTERVAL = check_setting_int(CFG, 'General', 'download_scan_interval', 5)
UPDATE_DB_INTERVAL = check_setting_int(CFG, 'General', 'update_db_interval', 24) UPDATE_DB_INTERVAL = check_setting_int(CFG, 'General', 'update_db_interval', 24)
MB_IGNORE_AGE = check_setting_int(CFG, 'General', 'mb_ignore_age', 365) MB_IGNORE_AGE = check_setting_int(CFG, 'General', 'mb_ignore_age', 365)
TORRENT_REMOVAL_INTERVAL = check_setting_int(CFG, 'General', 'torrent_removal_interval', 720)
TORRENTBLACKHOLE_DIR = check_setting_str(CFG, 'General', 'torrentblackhole_dir', '') TORRENTBLACKHOLE_DIR = check_setting_str(CFG, 'General', 'torrentblackhole_dir', '')
NUMBEROFSEEDERS = check_setting_str(CFG, 'General', 'numberofseeders', '10') NUMBEROFSEEDERS = check_setting_str(CFG, 'General', 'numberofseeders', '10')
ISOHUNT = bool(check_setting_int(CFG, 'General', 'isohunt', 0))
KAT = bool(check_setting_int(CFG, 'General', 'kat', 0))
PIRATEBAY = bool(check_setting_int(CFG, 'General', 'piratebay', 0))
PIRATEBAY_PROXY_URL = check_setting_str(CFG, 'General', 'piratebay_proxy_url', '')
MININOVA = bool(check_setting_int(CFG, 'General', 'mininova', 0))
DOWNLOAD_TORRENT_DIR = check_setting_str(CFG, 'General', 'download_torrent_dir', '') DOWNLOAD_TORRENT_DIR = check_setting_str(CFG, 'General', 'download_torrent_dir', '')
KAT = bool(check_setting_int(CFG, 'Kat', 'kat', 0))
KAT_PROXY_URL = check_setting_str(CFG, 'Kat', 'kat_proxy_url', '')
KAT_RATIO = check_setting_str(CFG, 'Kat', 'kat_ratio', '')
PIRATEBAY = bool(check_setting_int(CFG, 'Piratebay', 'piratebay', 0))
PIRATEBAY_PROXY_URL = check_setting_str(CFG, 'Piratebay', 'piratebay_proxy_url', '')
PIRATEBAY_RATIO = check_setting_str(CFG, 'Piratebay', 'piratebay_ratio', '')
MININOVA = bool(check_setting_int(CFG, 'Mininova', 'mininova', 0))
MININOVA_RATIO = check_setting_str(CFG, 'Mininova', 'mininova_ratio', '')
WAFFLES = bool(check_setting_int(CFG, 'Waffles', 'waffles', 0)) WAFFLES = bool(check_setting_int(CFG, 'Waffles', 'waffles', 0))
WAFFLES_UID = check_setting_str(CFG, 'Waffles', 'waffles_uid', '') WAFFLES_UID = check_setting_str(CFG, 'Waffles', 'waffles_uid', '')
WAFFLES_PASSKEY = check_setting_str(CFG, 'Waffles', 'waffles_passkey', '') WAFFLES_PASSKEY = check_setting_str(CFG, 'Waffles', 'waffles_passkey', '')
WAFFLES_RATIO = check_setting_str(CFG, 'Waffles', 'waffles_ratio', '')
RUTRACKER = bool(check_setting_int(CFG, 'Rutracker', 'rutracker', 0)) RUTRACKER = bool(check_setting_int(CFG, 'Rutracker', 'rutracker', 0))
RUTRACKER_USER = check_setting_str(CFG, 'Rutracker', 'rutracker_user', '') RUTRACKER_USER = check_setting_str(CFG, 'Rutracker', 'rutracker_user', '')
RUTRACKER_PASSWORD = check_setting_str(CFG, 'Rutracker', 'rutracker_password', '') RUTRACKER_PASSWORD = check_setting_str(CFG, 'Rutracker', 'rutracker_password', '')
RUTRACKER_RATIO = check_setting_str(CFG, 'Rutracker', 'rutracker_ratio', '')
WHATCD = bool(check_setting_int(CFG, 'What.cd', 'whatcd', 0)) WHATCD = bool(check_setting_int(CFG, 'What.cd', 'whatcd', 0))
WHATCD_USERNAME = check_setting_str(CFG, 'What.cd', 'whatcd_username', '') WHATCD_USERNAME = check_setting_str(CFG, 'What.cd', 'whatcd_username', '')
WHATCD_PASSWORD = check_setting_str(CFG, 'What.cd', 'whatcd_password', '') WHATCD_PASSWORD = check_setting_str(CFG, 'What.cd', 'whatcd_password', '')
WHATCD_RATIO = check_setting_str(CFG, 'What.cd', 'whatcd_ratio', '')
SAB_HOST = check_setting_str(CFG, 'SABnzbd', 'sab_host', '') SAB_HOST = check_setting_str(CFG, 'SABnzbd', 'sab_host', '')
SAB_USERNAME = check_setting_str(CFG, 'SABnzbd', 'sab_username', '') SAB_USERNAME = check_setting_str(CFG, 'SABnzbd', 'sab_username', '')
@@ -496,6 +513,7 @@ def initialize():
NZBGET_PASSWORD = check_setting_str(CFG, 'NZBget', 'nzbget_password', '') NZBGET_PASSWORD = check_setting_str(CFG, 'NZBget', 'nzbget_password', '')
NZBGET_CATEGORY = check_setting_str(CFG, 'NZBget', 'nzbget_category', '') NZBGET_CATEGORY = check_setting_str(CFG, 'NZBget', 'nzbget_category', '')
NZBGET_HOST = check_setting_str(CFG, 'NZBget', 'nzbget_host', '') NZBGET_HOST = check_setting_str(CFG, 'NZBget', 'nzbget_host', '')
NZBGET_PRIORITY = check_setting_int(CFG, 'NZBget', 'nzbget_priority', 0)
HEADPHONES_INDEXER = bool(check_setting_int(CFG, 'Headphones', 'headphones_indexer', 0)) HEADPHONES_INDEXER = bool(check_setting_int(CFG, 'Headphones', 'headphones_indexer', 0))
@@ -506,6 +524,7 @@ def initialize():
UTORRENT_HOST = check_setting_str(CFG, 'uTorrent', 'utorrent_host', '') UTORRENT_HOST = check_setting_str(CFG, 'uTorrent', 'utorrent_host', '')
UTORRENT_USERNAME = check_setting_str(CFG, 'uTorrent', 'utorrent_username', '') UTORRENT_USERNAME = check_setting_str(CFG, 'uTorrent', 'utorrent_username', '')
UTORRENT_PASSWORD = check_setting_str(CFG, 'uTorrent', 'utorrent_password', '') UTORRENT_PASSWORD = check_setting_str(CFG, 'uTorrent', 'utorrent_password', '')
UTORRENT_LABEL = check_setting_str(CFG, 'uTorrent', 'utorrent_label', '')
NEWZNAB = bool(check_setting_int(CFG, 'Newznab', 'newznab', 0)) NEWZNAB = bool(check_setting_int(CFG, 'Newznab', 'newznab', 0))
NEWZNAB_HOST = check_setting_str(CFG, 'Newznab', 'newznab_host', '') NEWZNAB_HOST = check_setting_str(CFG, 'Newznab', 'newznab_host', '')
@@ -520,10 +539,6 @@ def initialize():
NZBSORG_UID = check_setting_str(CFG, 'NZBsorg', 'nzbsorg_uid', '') NZBSORG_UID = check_setting_str(CFG, 'NZBsorg', 'nzbsorg_uid', '')
NZBSORG_HASH = check_setting_str(CFG, 'NZBsorg', 'nzbsorg_hash', '') NZBSORG_HASH = check_setting_str(CFG, 'NZBsorg', 'nzbsorg_hash', '')
NZBSRUS = bool(check_setting_int(CFG, 'NZBsRus', 'nzbsrus', 0))
NZBSRUS_UID = check_setting_str(CFG, 'NZBsRus', 'nzbsrus_uid', '')
NZBSRUS_APIKEY = check_setting_str(CFG, 'NZBsRus', 'nzbsrus_apikey', '')
OMGWTFNZBS = bool(check_setting_int(CFG, 'omgwtfnzbs', 'omgwtfnzbs', 0)) OMGWTFNZBS = bool(check_setting_int(CFG, 'omgwtfnzbs', 'omgwtfnzbs', 0))
OMGWTFNZBS_UID = check_setting_str(CFG, 'omgwtfnzbs', 'omgwtfnzbs_uid', '') OMGWTFNZBS_UID = check_setting_str(CFG, 'omgwtfnzbs', 'omgwtfnzbs_uid', '')
OMGWTFNZBS_APIKEY = check_setting_str(CFG, 'omgwtfnzbs', 'omgwtfnzbs_apikey', '') OMGWTFNZBS_APIKEY = check_setting_str(CFG, 'omgwtfnzbs', 'omgwtfnzbs_apikey', '')
@@ -609,7 +624,20 @@ def initialize():
TWITTER_USERNAME = check_setting_str(CFG, 'Twitter', 'twitter_username', '') TWITTER_USERNAME = check_setting_str(CFG, 'Twitter', 'twitter_username', '')
TWITTER_PASSWORD = check_setting_str(CFG, 'Twitter', 'twitter_password', '') TWITTER_PASSWORD = check_setting_str(CFG, 'Twitter', 'twitter_password', '')
TWITTER_PREFIX = check_setting_str(CFG, 'Twitter', 'twitter_prefix', 'Headphones') TWITTER_PREFIX = check_setting_str(CFG, 'Twitter', 'twitter_prefix', 'Headphones')
OSX_NOTIFY_ENABLED = bool(check_setting_int(CFG, 'OSX_Notify', 'osx_notify_enabled', 0))
OSX_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'OSX_Notify', 'osx_notify_onsnatch', 0))
OSX_NOTIFY_APP = check_setting_str(CFG, 'OSX_Notify', 'osx_notify_app', '/Applications/Headphones')
BOXCAR_ENABLED = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_enabled', 0))
BOXCAR_ONSNATCH = bool(check_setting_int(CFG, 'Boxcar', 'boxcar_onsnatch', 0))
BOXCAR_TOKEN = check_setting_str(CFG, 'Boxcar', 'boxcar_token', '')
SUBSONIC_ENABLED = bool(check_setting_int(CFG, 'Subsonic', 'subsonic_enabled', 0))
SUBSONIC_HOST = check_setting_str(CFG, 'Subsonic', 'subsonic_host', '')
SUBSONIC_USERNAME = check_setting_str(CFG, 'Subsonic', 'subsonic_username', '')
SUBSONIC_PASSWORD = check_setting_str(CFG, 'Subsonic', 'subsonic_password', '')
SONGKICK_ENABLED = bool(check_setting_int(CFG, 'Songkick', 'songkick_enabled', 1)) SONGKICK_ENABLED = bool(check_setting_int(CFG, 'Songkick', 'songkick_enabled', 1))
SONGKICK_APIKEY = check_setting_str(CFG, 'Songkick', 'songkick_apikey', 'nd1We7dFW2RqxPw8') SONGKICK_APIKEY = check_setting_str(CFG, 'Songkick', 'songkick_apikey', 'nd1We7dFW2RqxPw8')
SONGKICK_LOCATION = check_setting_str(CFG, 'Songkick', 'songkick_location', '') SONGKICK_LOCATION = check_setting_str(CFG, 'Songkick', 'songkick_location', '')
@@ -627,6 +655,8 @@ def initialize():
ALBUM_COMPLETION_PCT = check_setting_int(CFG, 'Advanced', 'album_completion_pct', 80) ALBUM_COMPLETION_PCT = check_setting_int(CFG, 'Advanced', 'album_completion_pct', 80)
VERIFY_SSL_CERT = bool(check_setting_int(CFG, 'Advanced', 'verify_ssl_cert', 1))
# update folder formats in the config & bump up config version # update folder formats in the config & bump up config version
if CONFIG_VERSION == '0': if CONFIG_VERSION == '0':
from headphones.helpers import replace_all from headphones.helpers import replace_all
@@ -676,10 +706,10 @@ def initialize():
CONFIG_VERSION = '3' CONFIG_VERSION = '3'
if CONFIG_VERSION == '3': if CONFIG_VERSION == '3':
#Update the BLACKHOLE option to the NZB_DOWNLOADER format #Update the BLACKHOLE option to the NZB_DOWNLOADER format
if BLACKHOLE: if BLACKHOLE:
NZB_DOWNLOADER = 2 NZB_DOWNLOADER = 2
CONFIG_VERSION = '4' CONFIG_VERSION = '4'
# Enable Headphones Indexer if they have a VIP account # Enable Headphones Indexer if they have a VIP account
if CONFIG_VERSION == '4': if CONFIG_VERSION == '4':
@@ -697,8 +727,8 @@ def initialize():
if VERBOSE: if VERBOSE:
sys.stderr.write('Unable to create the log directory. Logging to screen only.\n') sys.stderr.write('Unable to create the log directory. Logging to screen only.\n')
# Start the logger, silence console logging if we need to # Start the logger, disable console if needed
logger.initLogger(verbose=VERBOSE) logger.initLogger(console=not QUIET, verbose=VERBOSE)
if not CACHE_DIR: if not CACHE_DIR:
# Put the cache dir in the data dir for now # Put the cache dir in the data dir for now
@@ -804,10 +834,14 @@ def launch_browser(host, port, root):
try: try:
webbrowser.open('%s://%s:%i%s' % (protocol, host, port, root)) webbrowser.open('%s://%s:%i%s' % (protocol, host, port, root))
except Exception, e: except Exception as e:
logger.error('Could not launch browser: %s', e) logger.error('Could not launch browser: %s', e)
def config_write(): def config_write():
"""
Write configuration to file. If an IOError occures during a write, it will
be caught.
"""
new_config = ConfigObj(encoding="UTF-8") new_config = ConfigObj(encoding="UTF-8")
new_config.filename = CONFIG_FILE new_config.filename = CONFIG_FILE
@@ -846,14 +880,18 @@ def config_write():
new_config['General']['preferred_bitrate_low_buffer'] = PREFERRED_BITRATE_LOW_BUFFER new_config['General']['preferred_bitrate_low_buffer'] = PREFERRED_BITRATE_LOW_BUFFER
new_config['General']['preferred_bitrate_allow_lossless'] = int(PREFERRED_BITRATE_ALLOW_LOSSLESS) new_config['General']['preferred_bitrate_allow_lossless'] = int(PREFERRED_BITRATE_ALLOW_LOSSLESS)
new_config['General']['detect_bitrate'] = int(DETECT_BITRATE) new_config['General']['detect_bitrate'] = int(DETECT_BITRATE)
new_config['General']['lossless_bitrate_from'] = LOSSLESS_BITRATE_FROM
new_config['General']['lossless_bitrate_to'] = LOSSLESS_BITRATE_TO
new_config['General']['auto_add_artists'] = int(ADD_ARTISTS) new_config['General']['auto_add_artists'] = int(ADD_ARTISTS)
new_config['General']['correct_metadata'] = int(CORRECT_METADATA) new_config['General']['correct_metadata'] = int(CORRECT_METADATA)
new_config['General']['freeze_db'] = int(FREEZE_DB)
new_config['General']['move_files'] = int(MOVE_FILES) new_config['General']['move_files'] = int(MOVE_FILES)
new_config['General']['rename_files'] = int(RENAME_FILES) new_config['General']['rename_files'] = int(RENAME_FILES)
new_config['General']['folder_format'] = FOLDER_FORMAT new_config['General']['folder_format'] = FOLDER_FORMAT
new_config['General']['file_format'] = FILE_FORMAT new_config['General']['file_format'] = FILE_FORMAT
new_config['General']['file_underscores'] = int(FILE_UNDERSCORES) new_config['General']['file_underscores'] = int(FILE_UNDERSCORES)
new_config['General']['cleanup_files'] = int(CLEANUP_FILES) new_config['General']['cleanup_files'] = int(CLEANUP_FILES)
new_config['General']['keep_nfo'] = int(KEEP_NFO)
new_config['General']['add_album_art'] = int(ADD_ALBUM_ART) new_config['General']['add_album_art'] = int(ADD_ALBUM_ART)
new_config['General']['album_art_format'] = ALBUM_ART_FORMAT new_config['General']['album_art_format'] = ALBUM_ART_FORMAT
new_config['General']['embed_album_art'] = int(EMBED_ALBUM_ART) new_config['General']['embed_album_art'] = int(EMBED_ALBUM_ART)
@@ -874,27 +912,39 @@ def config_write():
new_config['General']['numberofseeders'] = NUMBEROFSEEDERS new_config['General']['numberofseeders'] = NUMBEROFSEEDERS
new_config['General']['torrentblackhole_dir'] = TORRENTBLACKHOLE_DIR new_config['General']['torrentblackhole_dir'] = TORRENTBLACKHOLE_DIR
new_config['General']['isohunt'] = int(ISOHUNT)
new_config['General']['kat'] = int(KAT)
new_config['General']['mininova'] = int(MININOVA)
new_config['General']['piratebay'] = int(PIRATEBAY)
new_config['General']['piratebay_proxy_url'] = PIRATEBAY_PROXY_URL
new_config['General']['download_torrent_dir'] = DOWNLOAD_TORRENT_DIR new_config['General']['download_torrent_dir'] = DOWNLOAD_TORRENT_DIR
new_config['Kat'] = {}
new_config['Kat']['kat'] = int(KAT)
new_config['Kat']['kat_proxy_url'] = KAT_PROXY_URL
new_config['Kat']['kat_ratio'] = KAT_RATIO
new_config['Mininova'] = {}
new_config['Mininova']['mininova'] = int(MININOVA)
new_config['Mininova']['mininova_ratio'] = MININOVA_RATIO
new_config['Piratebay'] = {}
new_config['Piratebay']['piratebay'] = int(PIRATEBAY)
new_config['Piratebay']['piratebay_proxy_url'] = PIRATEBAY_PROXY_URL
new_config['Piratebay']['piratebay_ratio'] = PIRATEBAY_RATIO
new_config['Waffles'] = {} new_config['Waffles'] = {}
new_config['Waffles']['waffles'] = int(WAFFLES) new_config['Waffles']['waffles'] = int(WAFFLES)
new_config['Waffles']['waffles_uid'] = WAFFLES_UID new_config['Waffles']['waffles_uid'] = WAFFLES_UID
new_config['Waffles']['waffles_passkey'] = WAFFLES_PASSKEY new_config['Waffles']['waffles_passkey'] = WAFFLES_PASSKEY
new_config['Waffles']['waffles_ratio'] = WAFFLES_RATIO
new_config['Rutracker'] = {} new_config['Rutracker'] = {}
new_config['Rutracker']['rutracker'] = int(RUTRACKER) new_config['Rutracker']['rutracker'] = int(RUTRACKER)
new_config['Rutracker']['rutracker_user'] = RUTRACKER_USER new_config['Rutracker']['rutracker_user'] = RUTRACKER_USER
new_config['Rutracker']['rutracker_password'] = RUTRACKER_PASSWORD new_config['Rutracker']['rutracker_password'] = RUTRACKER_PASSWORD
new_config['Rutracker']['rutracker_ratio'] = RUTRACKER_RATIO
new_config['What.cd'] = {} new_config['What.cd'] = {}
new_config['What.cd']['whatcd'] = int(WHATCD) new_config['What.cd']['whatcd'] = int(WHATCD)
new_config['What.cd']['whatcd_username'] = WHATCD_USERNAME new_config['What.cd']['whatcd_username'] = WHATCD_USERNAME
new_config['What.cd']['whatcd_password'] = WHATCD_PASSWORD new_config['What.cd']['whatcd_password'] = WHATCD_PASSWORD
new_config['What.cd']['whatcd_ratio'] = WHATCD_RATIO
new_config['General']['search_interval'] = SEARCH_INTERVAL new_config['General']['search_interval'] = SEARCH_INTERVAL
new_config['General']['libraryscan'] = int(LIBRARYSCAN) new_config['General']['libraryscan'] = int(LIBRARYSCAN)
@@ -902,6 +952,7 @@ def config_write():
new_config['General']['download_scan_interval'] = DOWNLOAD_SCAN_INTERVAL new_config['General']['download_scan_interval'] = DOWNLOAD_SCAN_INTERVAL
new_config['General']['update_db_interval'] = UPDATE_DB_INTERVAL new_config['General']['update_db_interval'] = UPDATE_DB_INTERVAL
new_config['General']['mb_ignore_age'] = MB_IGNORE_AGE new_config['General']['mb_ignore_age'] = MB_IGNORE_AGE
new_config['General']['torrent_removal_interval'] = TORRENT_REMOVAL_INTERVAL
new_config['SABnzbd'] = {} new_config['SABnzbd'] = {}
new_config['SABnzbd']['sab_host'] = SAB_HOST new_config['SABnzbd']['sab_host'] = SAB_HOST
@@ -915,6 +966,7 @@ def config_write():
new_config['NZBget']['nzbget_password'] = NZBGET_PASSWORD new_config['NZBget']['nzbget_password'] = NZBGET_PASSWORD
new_config['NZBget']['nzbget_category'] = NZBGET_CATEGORY new_config['NZBget']['nzbget_category'] = NZBGET_CATEGORY
new_config['NZBget']['nzbget_host'] = NZBGET_HOST new_config['NZBget']['nzbget_host'] = NZBGET_HOST
new_config['NZBget']['nzbget_priority'] = NZBGET_PRIORITY
new_config['Headphones'] = {} new_config['Headphones'] = {}
new_config['Headphones']['headphones_indexer'] = int(HEADPHONES_INDEXER) new_config['Headphones']['headphones_indexer'] = int(HEADPHONES_INDEXER)
@@ -928,6 +980,7 @@ def config_write():
new_config['uTorrent']['utorrent_host'] = UTORRENT_HOST new_config['uTorrent']['utorrent_host'] = UTORRENT_HOST
new_config['uTorrent']['utorrent_username'] = UTORRENT_USERNAME new_config['uTorrent']['utorrent_username'] = UTORRENT_USERNAME
new_config['uTorrent']['utorrent_password'] = UTORRENT_PASSWORD new_config['uTorrent']['utorrent_password'] = UTORRENT_PASSWORD
new_config['uTorrent']['utorrent_label'] = UTORRENT_LABEL
new_config['Newznab'] = {} new_config['Newznab'] = {}
new_config['Newznab']['newznab'] = int(NEWZNAB) new_config['Newznab']['newznab'] = int(NEWZNAB)
@@ -947,11 +1000,6 @@ def config_write():
new_config['NZBsorg']['nzbsorg_uid'] = NZBSORG_UID new_config['NZBsorg']['nzbsorg_uid'] = NZBSORG_UID
new_config['NZBsorg']['nzbsorg_hash'] = NZBSORG_HASH new_config['NZBsorg']['nzbsorg_hash'] = NZBSORG_HASH
new_config['NZBsRus'] = {}
new_config['NZBsRus']['nzbsrus'] = int(NZBSRUS)
new_config['NZBsRus']['nzbsrus_uid'] = NZBSRUS_UID
new_config['NZBsRus']['nzbsrus_apikey'] = NZBSRUS_APIKEY
new_config['omgwtfnzbs'] = {} new_config['omgwtfnzbs'] = {}
new_config['omgwtfnzbs']['omgwtfnzbs'] = int(OMGWTFNZBS) new_config['omgwtfnzbs']['omgwtfnzbs'] = int(OMGWTFNZBS)
new_config['omgwtfnzbs']['omgwtfnzbs_uid'] = OMGWTFNZBS_UID new_config['omgwtfnzbs']['omgwtfnzbs_uid'] = OMGWTFNZBS_UID
@@ -997,7 +1045,7 @@ def config_write():
new_config['NMA'] = {} new_config['NMA'] = {}
new_config['NMA']['nma_enabled'] = int(NMA_ENABLED) new_config['NMA']['nma_enabled'] = int(NMA_ENABLED)
new_config['NMA']['nma_apikey'] = NMA_APIKEY new_config['NMA']['nma_apikey'] = NMA_APIKEY
new_config['NMA']['nma_priority'] = NMA_PRIORITY new_config['NMA']['nma_priority'] = int(NMA_PRIORITY)
new_config['NMA']['nma_onsnatch'] = int(NMA_ONSNATCH) new_config['NMA']['nma_onsnatch'] = int(NMA_ONSNATCH)
new_config['Pushalot'] = {} new_config['Pushalot'] = {}
@@ -1025,6 +1073,22 @@ def config_write():
new_config['Twitter']['twitter_password'] = TWITTER_PASSWORD new_config['Twitter']['twitter_password'] = TWITTER_PASSWORD
new_config['Twitter']['twitter_prefix'] = TWITTER_PREFIX new_config['Twitter']['twitter_prefix'] = TWITTER_PREFIX
new_config['OSX_Notify'] = {}
new_config['OSX_Notify']['osx_notify_enabled'] = int(OSX_NOTIFY_ENABLED)
new_config['OSX_Notify']['osx_notify_onsnatch'] = int(OSX_NOTIFY_ONSNATCH)
new_config['OSX_Notify']['osx_notify_app'] = OSX_NOTIFY_APP
new_config['Boxcar'] = {}
new_config['Boxcar']['boxcar_enabled'] = int(BOXCAR_ENABLED)
new_config['Boxcar']['boxcar_onsnatch'] = int(BOXCAR_ONSNATCH)
new_config['Boxcar']['boxcar_token'] = BOXCAR_TOKEN
new_config['Subsonic'] = {}
new_config['Subsonic']['subsonic_enabled'] = int(SUBSONIC_ENABLED)
new_config['Subsonic']['subsonic_host'] = SUBSONIC_HOST
new_config['Subsonic']['subsonic_username'] = SUBSONIC_USERNAME
new_config['Subsonic']['subsonic_password'] = SUBSONIC_PASSWORD
new_config['Songkick'] = {} new_config['Songkick'] = {}
new_config['Songkick']['songkick_enabled'] = int(SONGKICK_ENABLED) new_config['Songkick']['songkick_enabled'] = int(SONGKICK_ENABLED)
new_config['Songkick']['songkick_apikey'] = SONGKICK_APIKEY new_config['Songkick']['songkick_apikey'] = SONGKICK_APIKEY
@@ -1065,9 +1129,15 @@ def config_write():
new_config['Advanced']['album_completion_pct'] = ALBUM_COMPLETION_PCT new_config['Advanced']['album_completion_pct'] = ALBUM_COMPLETION_PCT
new_config['Advanced']['cache_sizemb'] = CACHE_SIZEMB new_config['Advanced']['cache_sizemb'] = CACHE_SIZEMB
new_config['Advanced']['journal_mode'] = JOURNAL_MODE new_config['Advanced']['journal_mode'] = JOURNAL_MODE
new_config['Advanced']['verify_ssl_cert'] = int(VERIFY_SSL_CERT)
new_config.write() # Write it to file
logger.info("Writing configuration to file")
try:
new_config.write()
except IOError as e:
logger.error("Error writing configuration file: %s", e)
def start(): def start():
@@ -1076,7 +1146,7 @@ def start():
if __INITIALIZED__: if __INITIALIZED__:
# Start our scheduled background tasks # Start our scheduled background tasks
from headphones import updater, searcher, librarysync, postprocessor from headphones import updater, searcher, librarysync, postprocessor, torrentfinished
SCHED.add_interval_job(updater.dbUpdate, hours=UPDATE_DB_INTERVAL) SCHED.add_interval_job(updater.dbUpdate, hours=UPDATE_DB_INTERVAL)
SCHED.add_interval_job(searcher.searchforalbum, minutes=SEARCH_INTERVAL) SCHED.add_interval_job(searcher.searchforalbum, minutes=SEARCH_INTERVAL)
@@ -1085,15 +1155,19 @@ def start():
if CHECK_GITHUB: if CHECK_GITHUB:
SCHED.add_interval_job(versioncheck.checkGithub, minutes=CHECK_GITHUB_INTERVAL) SCHED.add_interval_job(versioncheck.checkGithub, minutes=CHECK_GITHUB_INTERVAL)
if DOWNLOAD_SCAN_INTERVAL > 0: if DOWNLOAD_SCAN_INTERVAL > 0:
SCHED.add_interval_job(postprocessor.checkFolder, minutes=DOWNLOAD_SCAN_INTERVAL) SCHED.add_interval_job(postprocessor.checkFolder, minutes=DOWNLOAD_SCAN_INTERVAL)
# Remove Torrent + data if Post Processed and finished Seeding
if TORRENT_REMOVAL_INTERVAL > 0:
SCHED.add_interval_job(torrentfinished.checkTorrentFinished, minutes=TORRENT_REMOVAL_INTERVAL)
SCHED.start() SCHED.start()
started = True started = True
def sig_handler(signum=None, frame=None): def sig_handler(signum=None, frame=None):
if type(signum) != type(None): if signum is not None:
logger.info("Signal %i caught, saving and exiting...", signum) logger.info("Signal %i caught, saving and exiting...", signum)
shutdown() shutdown()
+11 -11
View File
@@ -39,11 +39,11 @@ def switch(AlbumID, ReleaseID):
"ReleaseCountry": newalbumdata['ReleaseCountry'], "ReleaseCountry": newalbumdata['ReleaseCountry'],
"ReleaseFormat": newalbumdata['ReleaseFormat'] "ReleaseFormat": newalbumdata['ReleaseFormat']
} }
myDB.upsert("albums", newValueDict, controlValueDict) myDB.upsert("albums", newValueDict, controlValueDict)
for track in newtrackdata: for track in newtrackdata:
controlValueDict = {"TrackID": track['TrackID'], controlValueDict = {"TrackID": track['TrackID'],
"AlbumID": AlbumID} "AlbumID": AlbumID}
@@ -60,23 +60,23 @@ def switch(AlbumID, ReleaseID):
"Format": track['Format'], "Format": track['Format'],
"BitRate": track['BitRate'] "BitRate": track['BitRate']
} }
myDB.upsert("tracks", newValueDict, controlValueDict) myDB.upsert("tracks", newValueDict, controlValueDict)
# Mark albums as downloaded if they have at least 80% (by default, configurable) of the album # Mark albums as downloaded if they have at least 80% (by default, configurable) of the album
total_track_count = len(newtrackdata) total_track_count = len(newtrackdata)
have_track_count = len(myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', [AlbumID])) have_track_count = len(myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', [AlbumID]))
if oldalbumdata['Status'] == 'Skipped' and ((have_track_count/float(total_track_count)) >= (headphones.ALBUM_COMPLETION_PCT/100.0)): if oldalbumdata['Status'] == 'Skipped' and ((have_track_count/float(total_track_count)) >= (headphones.ALBUM_COMPLETION_PCT/100.0)):
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID]) myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID])
# Update have track counts on index # Update have track counts on index
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [newalbumdata['ArtistID']])) totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [newalbumdata['ArtistID']]))
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [newalbumdata['ArtistID']])) havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']]))
controlValueDict = {"ArtistID": newalbumdata['ArtistID']} controlValueDict = {"ArtistID": newalbumdata['ArtistID']}
newValueDict = { "TotalTracks": totaltracks, newValueDict = { "TotalTracks": totaltracks,
"HaveTracks": havetracks} "HaveTracks": havetracks}
myDB.upsert("artists", newValueDict, controlValueDict) myDB.upsert("artists", newValueDict, controlValueDict)
+95 -95
View File
@@ -13,37 +13,37 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
import headphones
from headphones import db, mb, importer, searcher, cache, postprocessor, versioncheck, logger from headphones import db, mb, importer, searcher, cache, postprocessor, versioncheck, logger
import lib.simplejson as simplejson
from xml.dom.minidom import Document from xml.dom.minidom import Document
import copy
cmd_list = [ 'getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSimilar', 'getHistory', 'getLogs', import headphones
import copy
import json
cmd_list = [ 'getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSimilar', 'getHistory', 'getLogs',
'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist', 'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist',
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'getVersion', 'checkGithub', 'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'getVersion', 'checkGithub',
'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo', 'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo',
'getArtistThumb', 'getAlbumThumb', 'choose_specific_download', 'download_specific_release'] 'getArtistThumb', 'getAlbumThumb', 'choose_specific_download', 'download_specific_release']
class Api(object): class Api(object):
def __init__(self): def __init__(self):
self.apikey = None self.apikey = None
self.cmd = None self.cmd = None
self.id = None self.id = None
self.kwargs = None self.kwargs = None
self.data = None self.data = None
self.callback = None self.callback = None
def checkParams(self,*args,**kwargs): def checkParams(self,*args,**kwargs):
if not headphones.API_ENABLED: if not headphones.API_ENABLED:
self.data = 'API not enabled' self.data = 'API not enabled'
return return
@@ -53,32 +53,32 @@ class Api(object):
if len(headphones.API_KEY) != 32: if len(headphones.API_KEY) != 32:
self.data = 'API key not generated correctly' self.data = 'API key not generated correctly'
return return
if 'apikey' not in kwargs: if 'apikey' not in kwargs:
self.data = 'Missing api key' self.data = 'Missing api key'
return return
if kwargs['apikey'] != headphones.API_KEY: if kwargs['apikey'] != headphones.API_KEY:
self.data = 'Incorrect API key' self.data = 'Incorrect API key'
return return
else: else:
self.apikey = kwargs.pop('apikey') self.apikey = kwargs.pop('apikey')
if 'cmd' not in kwargs: if 'cmd' not in kwargs:
self.data = 'Missing parameter: cmd' self.data = 'Missing parameter: cmd'
return return
if kwargs['cmd'] not in cmd_list: if kwargs['cmd'] not in cmd_list:
self.data = 'Unknown command: %s' % kwargs['cmd'] self.data = 'Unknown command: %s' % kwargs['cmd']
return return
else: else:
self.cmd = kwargs.pop('cmd') self.cmd = kwargs.pop('cmd')
self.kwargs = kwargs self.kwargs = kwargs
self.data = 'OK' self.data = 'OK'
def fetchData(self): def fetchData(self):
if self.data == 'OK': if self.data == 'OK':
logger.info('Recieved API command: %s', self.cmd) logger.info('Recieved API command: %s', self.cmd)
methodToCall = getattr(self, "_" + self.cmd) methodToCall = getattr(self, "_" + self.cmd)
@@ -87,82 +87,82 @@ class Api(object):
if type(self.data) == type(''): if type(self.data) == type(''):
return self.data return self.data
else: else:
return simplejson.dumps(self.data) return json.dumps(self.data)
else: else:
self.callback = self.kwargs['callback'] self.callback = self.kwargs['callback']
self.data = simplejson.dumps(self.data) self.data = json.dumps(self.data)
self.data = self.callback + '(' + self.data + ');' self.data = self.callback + '(' + self.data + ');'
return self.data return self.data
else: else:
return self.data return self.data
def _dic_from_query(self,query): def _dic_from_query(self,query):
myDB = db.DBConnection() myDB = db.DBConnection()
rows = myDB.select(query) rows = myDB.select(query)
rows_as_dic = [] rows_as_dic = []
for row in rows: for row in rows:
row_as_dic = dict(zip(row.keys(), row)) row_as_dic = dict(zip(row.keys(), row))
rows_as_dic.append(row_as_dic) rows_as_dic.append(row_as_dic)
return rows_as_dic return rows_as_dic
def _getIndex(self, **kwargs): def _getIndex(self, **kwargs):
self.data = self._dic_from_query('SELECT * from artists order by ArtistSortName COLLATE NOCASE') self.data = self._dic_from_query('SELECT * from artists order by ArtistSortName COLLATE NOCASE')
return return
def _getArtist(self, **kwargs): def _getArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
artist = self._dic_from_query('SELECT * from artists WHERE ArtistID="' + self.id + '"') artist = self._dic_from_query('SELECT * from artists WHERE ArtistID="' + self.id + '"')
albums = self._dic_from_query('SELECT * from albums WHERE ArtistID="' + self.id + '" order by ReleaseDate DESC') albums = self._dic_from_query('SELECT * from albums WHERE ArtistID="' + self.id + '" order by ReleaseDate DESC')
description = self._dic_from_query('SELECT * from descriptions WHERE ArtistID="' + self.id + '"') description = self._dic_from_query('SELECT * from descriptions WHERE ArtistID="' + self.id + '"')
self.data = { 'artist': artist, 'albums': albums, 'description' : description } self.data = { 'artist': artist, 'albums': albums, 'description' : description }
return return
def _getAlbum(self, **kwargs): def _getAlbum(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
album = self._dic_from_query('SELECT * from albums WHERE AlbumID="' + self.id + '"') album = self._dic_from_query('SELECT * from albums WHERE AlbumID="' + self.id + '"')
tracks = self._dic_from_query('SELECT * from tracks WHERE AlbumID="' + self.id + '"') tracks = self._dic_from_query('SELECT * from tracks WHERE AlbumID="' + self.id + '"')
description = self._dic_from_query('SELECT * from descriptions WHERE ReleaseGroupID="' + self.id + '"') description = self._dic_from_query('SELECT * from descriptions WHERE ReleaseGroupID="' + self.id + '"')
self.data = { 'album' : album, 'tracks' : tracks, 'description' : description } self.data = { 'album' : album, 'tracks' : tracks, 'description' : description }
return return
def _getHistory(self, **kwargs): def _getHistory(self, **kwargs):
self.data = self._dic_from_query('SELECT * from snatched order by DateAdded DESC') self.data = self._dic_from_query('SELECT * from snatched WHERE status NOT LIKE "Seed%" order by DateAdded DESC')
return return
def _getUpcoming(self, **kwargs): def _getUpcoming(self, **kwargs):
self.data = self._dic_from_query("SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate DESC") self.data = self._dic_from_query("SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate DESC")
return return
def _getWanted(self, **kwargs): def _getWanted(self, **kwargs):
self.data = self._dic_from_query("SELECT * from albums WHERE Status='Wanted'") self.data = self._dic_from_query("SELECT * from albums WHERE Status='Wanted'")
return return
def _getSimilar(self, **kwargs): def _getSimilar(self, **kwargs):
self.data = self._dic_from_query('SELECT * from lastfmcloud') self.data = self._dic_from_query('SELECT * from lastfmcloud')
return return
def _getLogs(self, **kwargs): def _getLogs(self, **kwargs):
pass pass
def _findArtist(self, **kwargs): def _findArtist(self, **kwargs):
if 'name' not in kwargs: if 'name' not in kwargs:
self.data = 'Missing parameter: name' self.data = 'Missing parameter: name'
@@ -171,7 +171,7 @@ class Api(object):
limit = kwargs['limit'] limit = kwargs['limit']
else: else:
limit=50 limit=50
self.data = mb.findArtist(kwargs['name'], limit) self.data = mb.findArtist(kwargs['name'], limit)
def _findAlbum(self, **kwargs): def _findAlbum(self, **kwargs):
@@ -182,216 +182,216 @@ class Api(object):
limit = kwargs['limit'] limit = kwargs['limit']
else: else:
limit=50 limit=50
self.data = mb.findRelease(kwargs['name'], limit) self.data = mb.findRelease(kwargs['name'], limit)
def _addArtist(self, **kwargs): def _addArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
try: try:
importer.addArtisttoDB(self.id) importer.addArtisttoDB(self.id)
except Exception, e: except Exception, e:
self.data = e self.data = e
return return
def _delArtist(self, **kwargs): def _delArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
myDB = db.DBConnection() myDB = db.DBConnection()
myDB.action('DELETE from artists WHERE ArtistID="' + self.id + '"') myDB.action('DELETE from artists WHERE ArtistID="' + self.id + '"')
myDB.action('DELETE from albums WHERE ArtistID="' + self.id + '"') myDB.action('DELETE from albums WHERE ArtistID="' + self.id + '"')
myDB.action('DELETE from tracks WHERE ArtistID="' + self.id + '"') myDB.action('DELETE from tracks WHERE ArtistID="' + self.id + '"')
def _pauseArtist(self, **kwargs): def _pauseArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
myDB = db.DBConnection() myDB = db.DBConnection()
controlValueDict = {'ArtistID': self.id} controlValueDict = {'ArtistID': self.id}
newValueDict = {'Status': 'Paused'} newValueDict = {'Status': 'Paused'}
myDB.upsert("artists", newValueDict, controlValueDict) myDB.upsert("artists", newValueDict, controlValueDict)
def _resumeArtist(self, **kwargs): def _resumeArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
myDB = db.DBConnection() myDB = db.DBConnection()
controlValueDict = {'ArtistID': self.id} controlValueDict = {'ArtistID': self.id}
newValueDict = {'Status': 'Active'} newValueDict = {'Status': 'Active'}
myDB.upsert("artists", newValueDict, controlValueDict) myDB.upsert("artists", newValueDict, controlValueDict)
def _refreshArtist(self, **kwargs): def _refreshArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
try: try:
importer.addArtisttoDB(self.id) importer.addArtisttoDB(self.id)
except Exception, e: except Exception, e:
self.data = e self.data = e
return return
def _addAlbum(self, **kwargs): def _addAlbum(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
try: try:
importer.addReleaseById(self.id) importer.addReleaseById(self.id)
except Exception, e: except Exception, e:
self.data = e self.data = e
return return
def _queueAlbum(self, **kwargs): def _queueAlbum(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
if 'new' in kwargs: if 'new' in kwargs:
new = kwargs['new'] new = kwargs['new']
else: else:
new = False new = False
if 'lossless' in kwargs: if 'lossless' in kwargs:
lossless = kwargs['lossless'] lossless = kwargs['lossless']
else: else:
lossless = False lossless = False
myDB = db.DBConnection() myDB = db.DBConnection()
controlValueDict = {'AlbumID': self.id} controlValueDict = {'AlbumID': self.id}
if lossless: if lossless:
newValueDict = {'Status': 'Wanted Lossless'} newValueDict = {'Status': 'Wanted Lossless'}
else: else:
newValueDict = {'Status': 'Wanted'} newValueDict = {'Status': 'Wanted'}
myDB.upsert("albums", newValueDict, controlValueDict) myDB.upsert("albums", newValueDict, controlValueDict)
searcher.searchforalbum(self.id, new) searcher.searchforalbum(self.id, new)
def _unqueueAlbum(self, **kwargs): def _unqueueAlbum(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
myDB = db.DBConnection() myDB = db.DBConnection()
controlValueDict = {'AlbumID': self.id} controlValueDict = {'AlbumID': self.id}
newValueDict = {'Status': 'Skipped'} newValueDict = {'Status': 'Skipped'}
myDB.upsert("albums", newValueDict, controlValueDict) myDB.upsert("albums", newValueDict, controlValueDict)
def _forceSearch(self, **kwargs): def _forceSearch(self, **kwargs):
searcher.searchforalbum() searcher.searchforalbum()
def _forceProcess(self, **kwargs): def _forceProcess(self, **kwargs):
self.dir = None self.dir = None
if 'dir' in kwargs: if 'dir' in kwargs:
self.dir = kwargs['dir'] self.dir = kwargs['dir']
postprocessor.forcePostProcess(self.dir) postprocessor.forcePostProcess(self.dir)
def _getVersion(self, **kwargs): def _getVersion(self, **kwargs):
self.data = { self.data = {
'git_path' : headphones.GIT_PATH, 'git_path' : headphones.GIT_PATH,
'install_type' : headphones.INSTALL_TYPE, 'install_type' : headphones.INSTALL_TYPE,
'current_version' : headphones.CURRENT_VERSION, 'current_version' : headphones.CURRENT_VERSION,
'latest_version' : headphones.LATEST_VERSION, 'latest_version' : headphones.LATEST_VERSION,
'commits_behind' : headphones.COMMITS_BEHIND, 'commits_behind' : headphones.COMMITS_BEHIND,
} }
def _checkGithub(self, **kwargs): def _checkGithub(self, **kwargs):
versioncheck.checkGithub() versioncheck.checkGithub()
self._getVersion() self._getVersion()
def _shutdown(self, **kwargs): def _shutdown(self, **kwargs):
headphones.SIGNAL = 'shutdown' headphones.SIGNAL = 'shutdown'
def _restart(self, **kwargs): def _restart(self, **kwargs):
headphones.SIGNAL = 'restart' headphones.SIGNAL = 'restart'
def _update(self, **kwargs): def _update(self, **kwargs):
headphones.SIGNAL = 'update' headphones.SIGNAL = 'update'
def _getArtistArt(self, **kwargs): def _getArtistArt(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getArtwork(ArtistID=self.id) self.data = cache.getArtwork(ArtistID=self.id)
def _getAlbumArt(self, **kwargs): def _getAlbumArt(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getArtwork(AlbumID=self.id) self.data = cache.getArtwork(AlbumID=self.id)
def _getArtistInfo(self, **kwargs): def _getArtistInfo(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getInfo(ArtistID=self.id) self.data = cache.getInfo(ArtistID=self.id)
def _getAlbumInfo(self, **kwargs): def _getAlbumInfo(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getInfo(AlbumID=self.id) self.data = cache.getInfo(AlbumID=self.id)
def _getArtistThumb(self, **kwargs): def _getArtistThumb(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getThumb(ArtistID=self.id) self.data = cache.getThumb(ArtistID=self.id)
def _getAlbumThumb(self, **kwargs): def _getAlbumThumb(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getThumb(AlbumID=self.id) self.data = cache.getThumb(AlbumID=self.id)
def _choose_specific_download(self, **kwargs): def _choose_specific_download(self, **kwargs):
@@ -403,9 +403,9 @@ class Api(object):
self.id = kwargs['id'] self.id = kwargs['id']
results = searcher.searchforalbum(self.id, choose_specific_download=True) results = searcher.searchforalbum(self.id, choose_specific_download=True)
results_as_dicts = [] results_as_dicts = []
for result in results: for result in results:
result_dict = { result_dict = {
+42 -47
View File
@@ -20,7 +20,7 @@ import headphones
from headphones import db, helpers, logger, lastfm, request from headphones import db, helpers, logger, lastfm, request
lastfm_apikey = "690e1ed3bc00bc91804cd8f7fe5ed6d4" LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
class Cache(object): class Cache(object):
""" """
@@ -42,25 +42,22 @@ class Cache(object):
path_to_art_cache = os.path.join(headphones.CACHE_DIR, 'artwork') path_to_art_cache = os.path.join(headphones.CACHE_DIR, 'artwork')
id = None
id_type = None # 'artist' or 'album' - set automatically depending on whether ArtistID or AlbumID is passed
query_type = None # 'artwork','thumb' or 'info' - set automatically
artwork_files = []
thumb_files = []
artwork_errors = False
artwork_url = None
thumb_errors = False
thumb_url = None
info_summary = None
info_content = None
def __init__(self): def __init__(self):
self.id = None
self.id_type = None # 'artist' or 'album' - set automatically depending on whether ArtistID or AlbumID is passed
self.query_type = None # 'artwork','thumb' or 'info' - set automatically
pass self.artwork_files = []
self.thumb_files = []
self.artwork_errors = False
self.artwork_url = None
self.thumb_errors = False
self.thumb_url = None
self.info_summary = None
self.info_content = None
def _findfilesstartingwith(self,pattern,folder): def _findfilesstartingwith(self,pattern,folder):
files = [] files = []
@@ -126,9 +123,9 @@ class Cache(object):
return thumb_url return thumb_url
def get_artwork_from_cache(self, ArtistID=None, AlbumID=None): def get_artwork_from_cache(self, ArtistID=None, AlbumID=None):
''' """
Pass a musicbrainz id to this function (either ArtistID or AlbumID) Pass a musicbrainz id to this function (either ArtistID or AlbumID)
''' """
self.query_type = 'artwork' self.query_type = 'artwork'
@@ -152,9 +149,9 @@ class Cache(object):
return None return None
def get_thumb_from_cache(self, ArtistID=None, AlbumID=None): def get_thumb_from_cache(self, ArtistID=None, AlbumID=None):
''' """
Pass a musicbrainz id to this function (either ArtistID or AlbumID) Pass a musicbrainz id to this function (either ArtistID or AlbumID)
''' """
self.query_type = 'thumb' self.query_type = 'thumb'
@@ -209,14 +206,14 @@ class Cache(object):
if ArtistID: if ArtistID:
self.id_type = 'artist' self.id_type = 'artist'
data = lastfm.request_lastfm("artist.getinfo", mbid=ArtistID, api_key=lastfm_apikey) data = lastfm.request_lastfm("artist.getinfo", mbid=ArtistID, api_key=LASTFM_API_KEY)
if not data: if not data:
return return
try: try:
image_url = data['artist']['image'][-1]['#text'] image_url = data['artist']['image'][-1]['#text']
except Exception: except (KeyError, IndexError):
logger.debug('No artist image found') logger.debug('No artist image found')
image_url = None image_url = None
@@ -227,21 +224,21 @@ class Cache(object):
else: else:
self.id_type = 'album' self.id_type = 'album'
data = lastfm.request_lastfm("album.getinfo", mbid=AlbumID, api_key=lastfm_apikey) data = lastfm.request_lastfm("album.getinfo", mbid=AlbumID, api_key=LASTFM_API_KEY)
if not data: if not data:
return return
try: try:
image_url = data['artist']['image'][-1]['#text'] image_url = data['album']['image'][-1]['#text']
except Exception: except (KeyError, IndexError):
logger.debug('No artist image found') logger.debug('No album image found on last.fm')
image_url = None image_url = None
thumb_url = self._get_thumb_url(data) thumb_url = self._get_thumb_url(data)
if not thumb_url: if not thumb_url:
logger.debug('No artist thumbnail image found') logger.debug('No album thumbnail image found on last.fm')
return {'artwork' : image_url, 'thumbnail' : thumb_url } return {'artwork' : image_url, 'thumbnail' : thumb_url }
@@ -254,24 +251,24 @@ class Cache(object):
# Since lastfm uses release ids rather than release group ids for albums, we have to do a artist + album search for albums # Since lastfm uses release ids rather than release group ids for albums, we have to do a artist + album search for albums
if self.id_type == 'artist': if self.id_type == 'artist':
data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=lastfm_apikey) data = lastfm.request_lastfm("artist.getinfo", mbid=self.id, api_key=LASTFM_API_KEY)
if not data: if not data:
return return
try: try:
self.info_summary = data['artist']['bio']['summary'] self.info_summary = data['artist']['bio']['summary']
except Exception: except KeyError:
logger.debug('No artist bio summary found') logger.debug('No artist bio summary found')
self.info_summary = None self.info_summary = None
try: try:
self.info_content = data['artist']['bio']['content'] self.info_content = data['artist']['bio']['content']
except Exception: except KeyError:
logger.debug('No artist bio found') logger.debug('No artist bio found')
self.info_content = None self.info_content = None
try: try:
image_url = data['artist']['image'][-1]['#text'] image_url = data['artist']['image'][-1]['#text']
except Exception: except KeyError:
logger.debug('No artist image found') logger.debug('No artist image found')
image_url = None image_url = None
@@ -282,24 +279,24 @@ class Cache(object):
else: else:
dbartist = myDB.action('SELECT ArtistName, AlbumTitle FROM albums WHERE AlbumID=?', [self.id]).fetchone() dbartist = myDB.action('SELECT ArtistName, AlbumTitle FROM albums WHERE AlbumID=?', [self.id]).fetchone()
data = lastfm.request_lastfm("album.getinfo", artist=dbartist['ArtistName'], album=dbartist['AlbumTitle'], api_key=lastfm_apikey) data = lastfm.request_lastfm("album.getinfo", artist=dbartist['ArtistName'], album=dbartist['AlbumTitle'], api_key=LASTFM_API_KEY)
if not data: if not data:
return return
try: try:
self.info_summary = data['album']['wiki']['summary'] self.info_summary = data['album']['wiki']['summary']
except Exception: except KeyError:
logger.debug('No album summary found') logger.debug('No album summary found')
self.info_summary = None self.info_summary = None
try: try:
self.info_content = data['album']['wiki']['content'] self.info_content = data['album']['wiki']['content']
except Exception: except KeyError:
logger.debug('No album infomation found') logger.debug('No album infomation found')
self.info_content = None self.info_content = None
try: try:
image_url = data['album']['image'][-1]['#text'] image_url = data['album']['image'][-1]['#text']
except Exception: except KeyError:
logger.debug('No album image link found') logger.debug('No album image link found')
image_url = None image_url = None
@@ -359,10 +356,9 @@ class Cache(object):
artwork_path = os.path.join(self.path_to_art_cache, self.id + '.' + helpers.today() + ext) artwork_path = os.path.join(self.path_to_art_cache, self.id + '.' + helpers.today() + ext)
try: try:
f = open(artwork_path, 'wb') with open(artwork_path, 'wb') as f:
f.write(artwork) f.write(artwork)
f.close() except IOError as e:
except Exception, e:
logger.error('Unable to write to the cache dir: %s', e) logger.error('Unable to write to the cache dir: %s', e)
self.artwork_errors = True self.artwork_errors = True
self.artwork_url = image_url self.artwork_url = image_url
@@ -376,7 +372,7 @@ class Cache(object):
if not os.path.isdir(self.path_to_art_cache): if not os.path.isdir(self.path_to_art_cache):
try: try:
os.makedirs(self.path_to_art_cache) os.makedirs(self.path_to_art_cache)
except Exception, e: except Exception as e:
logger.error('Unable to create artwork cache dir. Error: %s' + e) logger.error('Unable to create artwork cache dir. Error: %s' + e)
self.thumb_errors = True self.thumb_errors = True
self.thumb_url = thumb_url self.thumb_url = thumb_url
@@ -385,17 +381,16 @@ class Cache(object):
for thumb_file in self.thumb_files: for thumb_file in self.thumb_files:
try: try:
os.remove(thumb_file) os.remove(thumb_file)
except: except Exception as e:
logger.error('Error deleting file from the cache: %s', thumb_file) logger.error('Error deleting file from the cache: %s', thumb_file)
ext = os.path.splitext(image_url)[1] ext = os.path.splitext(image_url)[1]
thumb_path = os.path.join(self.path_to_art_cache, 'T_' + self.id + '.' + helpers.today() + ext) thumb_path = os.path.join(self.path_to_art_cache, 'T_' + self.id + '.' + helpers.today() + ext)
try: try:
f = open(thumb_path, 'wb') with open(thumb_path, 'wb') as f:
f.write(artwork) f.write(artwork)
f.close() except IOError as e:
except Exception, e:
logger.error('Unable to write to the cache dir: %s', e) logger.error('Unable to write to the cache dir: %s', e)
self.thumb_errors = True self.thumb_errors = True
self.thumb_url = image_url self.thumb_url = image_url
+130 -130
View File
@@ -1,130 +1,130 @@
# This file is part of Headphones. # This file is part of Headphones.
# #
# Headphones is free software: you can redistribute it and/or modify # Headphones is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# Headphones is distributed in the hope that it will be useful, # Headphones is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
######################################### #########################################
## Stolen from Sick-Beard's classes.py ## ## Stolen from Sick-Beard's classes.py ##
######################################### #########################################
import headphones import headphones
import urllib import urllib
import datetime import datetime
from common import USER_AGENT from common import USER_AGENT
class HeadphonesURLopener(urllib.FancyURLopener): class HeadphonesURLopener(urllib.FancyURLopener):
version = USER_AGENT version = USER_AGENT
class AuthURLOpener(HeadphonesURLopener): class AuthURLOpener(HeadphonesURLopener):
""" """
URLOpener class that supports http auth without needing interactive password entry. URLOpener class that supports http auth without needing interactive password entry.
If the provided username/password don't work it simply fails. If the provided username/password don't work it simply fails.
user: username to use for HTTP auth user: username to use for HTTP auth
pw: password to use for HTTP auth pw: password to use for HTTP auth
""" """
def __init__(self, user, pw): def __init__(self, user, pw):
self.username = user self.username = user
self.password = pw self.password = pw
# remember if we've tried the username/password before # remember if we've tried the username/password before
self.numTries = 0 self.numTries = 0
# call the base class # call the base class
urllib.FancyURLopener.__init__(self) urllib.FancyURLopener.__init__(self)
def prompt_user_passwd(self, host, realm): def prompt_user_passwd(self, host, realm):
""" """
Override this function and instead of prompting just give the Override this function and instead of prompting just give the
username/password that were provided when the class was instantiated. username/password that were provided when the class was instantiated.
""" """
# if this is the first try then provide a username/password # if this is the first try then provide a username/password
if self.numTries == 0: if self.numTries == 0:
self.numTries = 1 self.numTries = 1
return (self.username, self.password) return (self.username, self.password)
# if we've tried before then return blank which cancels the request # if we've tried before then return blank which cancels the request
else: else:
return ('', '') return ('', '')
# this is pretty much just a hack for convenience # this is pretty much just a hack for convenience
def openit(self, url): def openit(self, url):
self.numTries = 0 self.numTries = 0
return HeadphonesURLopener.open(self, url) return HeadphonesURLopener.open(self, url)
class SearchResult: class SearchResult:
""" """
Represents a search result from an indexer. Represents a search result from an indexer.
""" """
def __init__(self): def __init__(self):
self.provider = -1 self.provider = -1
# URL to the NZB/torrent file # URL to the NZB/torrent file
self.url = "" self.url = ""
# used by some providers to store extra info associated with the result # used by some providers to store extra info associated with the result
self.extraInfo = [] self.extraInfo = []
# quality of the release # quality of the release
self.quality = -1 self.quality = -1
# release name # release name
self.name = "" self.name = ""
def __str__(self): def __str__(self):
if self.provider == None: if self.provider == None:
return "Invalid provider, unable to print self" return "Invalid provider, unable to print self"
myString = self.provider.name + " @ " + self.url + "\n" myString = self.provider.name + " @ " + self.url + "\n"
myString += "Extra Info:\n" myString += "Extra Info:\n"
for extra in self.extraInfo: for extra in self.extraInfo:
myString += " " + extra + "\n" myString += " " + extra + "\n"
return myString return myString
class NZBSearchResult(SearchResult): class NZBSearchResult(SearchResult):
""" """
Regular NZB result with an URL to the NZB Regular NZB result with an URL to the NZB
""" """
resultType = "nzb" resultType = "nzb"
class NZBDataSearchResult(SearchResult): class NZBDataSearchResult(SearchResult):
""" """
NZB result where the actual NZB XML data is stored in the extraInfo NZB result where the actual NZB XML data is stored in the extraInfo
""" """
resultType = "nzbdata" resultType = "nzbdata"
class TorrentSearchResult(SearchResult): class TorrentSearchResult(SearchResult):
""" """
Torrent result with an URL to the torrent Torrent result with an URL to the torrent
""" """
resultType = "torrent" resultType = "torrent"
class Proper: class Proper:
def __init__(self, name, url, date): def __init__(self, name, url, date):
self.name = name self.name = name
self.url = url self.url = url
self.date = date self.date = date
self.provider = None self.provider = None
self.quality = -1 self.quality = -1
self.tvdbid = -1 self.tvdbid = -1
self.season = -1 self.season = -1
self.episode = -1 self.episode = -1
def __str__(self): def __str__(self):
return str(self.date)+" "+self.name+" "+str(self.season)+"x"+str(self.episode)+" of "+str(self.tvdbid) return str(self.date)+" "+self.name+" "+str(self.season)+"x"+str(self.episode)+" of "+str(self.tvdbid)
+177 -177
View File
@@ -1,177 +1,177 @@
# This file is part of Headphones. # This file is part of Headphones.
# #
# Headphones is free software: you can redistribute it and/or modify # Headphones is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# Headphones is distributed in the hope that it will be useful, # Headphones is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
''' '''
Created on Aug 1, 2011 Created on Aug 1, 2011
@author: Michael @author: Michael
''' '''
import platform, operator, os, re import platform, operator, os, re
from headphones import version from headphones import version
#Identify Our Application #Identify Our Application
USER_AGENT = 'Headphones/-'+version.HEADPHONES_VERSION+' ('+platform.system()+' '+platform.release()+')' USER_AGENT = 'Headphones/-'+version.HEADPHONES_VERSION+' ('+platform.system()+' '+platform.release()+')'
### Notification Types ### Notification Types
NOTIFY_SNATCH = 1 NOTIFY_SNATCH = 1
NOTIFY_DOWNLOAD = 2 NOTIFY_DOWNLOAD = 2
notifyStrings = {} notifyStrings = {}
notifyStrings[NOTIFY_SNATCH] = "Started Download" notifyStrings[NOTIFY_SNATCH] = "Started Download"
notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished" notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished"
### Release statuses ### Release statuses
UNKNOWN = -1 # should never happen UNKNOWN = -1 # should never happen
UNAIRED = 1 # releases that haven't dropped yet UNAIRED = 1 # releases that haven't dropped yet
SNATCHED = 2 # qualified with quality SNATCHED = 2 # qualified with quality
WANTED = 3 # releases we don't have but want to get WANTED = 3 # releases we don't have but want to get
DOWNLOADED = 4 # qualified with quality DOWNLOADED = 4 # qualified with quality
SKIPPED = 5 # releases we don't want SKIPPED = 5 # releases we don't want
ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats) ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats)
IGNORED = 7 # releases that you don't want included in your download stats IGNORED = 7 # releases that you don't want included in your download stats
SNATCHED_PROPER = 9 # qualified with quality SNATCHED_PROPER = 9 # qualified with quality
class Quality: class Quality:
NONE = 0 NONE = 0
B192 = 1<<1 # 2 B192 = 1<<1 # 2
VBR = 1<<2 # 4 VBR = 1<<2 # 4
B256 = 1<<3 # 8 B256 = 1<<3 # 8
B320 = 1<<4 #16 B320 = 1<<4 #16
FLAC = 1<<5 #32 FLAC = 1<<5 #32
# put these bits at the other end of the spectrum, far enough out that they shouldn't interfere # put these bits at the other end of the spectrum, far enough out that they shouldn't interfere
UNKNOWN = 1<<15 UNKNOWN = 1<<15
qualityStrings = {NONE: "N/A", qualityStrings = {NONE: "N/A",
UNKNOWN: "Unknown", UNKNOWN: "Unknown",
B192: "MP3 192", B192: "MP3 192",
VBR: "MP3 VBR", VBR: "MP3 VBR",
B256: "MP3 256", B256: "MP3 256",
B320: "MP3 320", B320: "MP3 320",
FLAC: "Flac"} FLAC: "Flac"}
statusPrefixes = {DOWNLOADED: "Downloaded", statusPrefixes = {DOWNLOADED: "Downloaded",
SNATCHED: "Snatched"} SNATCHED: "Snatched"}
@staticmethod @staticmethod
def _getStatusStrings(status): def _getStatusStrings(status):
toReturn = {} toReturn = {}
for x in Quality.qualityStrings.keys(): for x in Quality.qualityStrings.keys():
toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status]+" ("+Quality.qualityStrings[x]+")" toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status]+" ("+Quality.qualityStrings[x]+")"
return toReturn return toReturn
@staticmethod @staticmethod
def combineQualities(anyQualities, bestQualities): def combineQualities(anyQualities, bestQualities):
anyQuality = 0 anyQuality = 0
bestQuality = 0 bestQuality = 0
if anyQualities: if anyQualities:
anyQuality = reduce(operator.or_, anyQualities) anyQuality = reduce(operator.or_, anyQualities)
if bestQualities: if bestQualities:
bestQuality = reduce(operator.or_, bestQualities) bestQuality = reduce(operator.or_, bestQualities)
return anyQuality | (bestQuality<<16) return anyQuality | (bestQuality<<16)
@staticmethod @staticmethod
def splitQuality(quality): def splitQuality(quality):
anyQualities = [] anyQualities = []
bestQualities = [] bestQualities = []
for curQual in Quality.qualityStrings.keys(): for curQual in Quality.qualityStrings.keys():
if curQual & quality: if curQual & quality:
anyQualities.append(curQual) anyQualities.append(curQual)
if curQual<<16 & quality: if curQual<<16 & quality:
bestQualities.append(curQual) bestQualities.append(curQual)
return (anyQualities, bestQualities) return (anyQualities, bestQualities)
@staticmethod @staticmethod
def nameQuality(name): def nameQuality(name):
name = os.path.basename(name) name = os.path.basename(name)
# if we have our exact text then assume we put it there # if we have our exact text then assume we put it there
for x in Quality.qualityStrings: for x in Quality.qualityStrings:
if x == Quality.UNKNOWN: if x == Quality.UNKNOWN:
continue continue
regex = '\W'+Quality.qualityStrings[x].replace(' ','\W')+'\W' regex = '\W'+Quality.qualityStrings[x].replace(' ','\W')+'\W'
regex_match = re.search(regex, name, re.I) regex_match = re.search(regex, name, re.I)
if regex_match: if regex_match:
return x return x
checkName = lambda list, func: func([re.search(x, name, re.I) for x in list]) checkName = lambda list, func: func([re.search(x, name, re.I) for x in list])
#TODO: fix quality checking here #TODO: fix quality checking here
if checkName(["mp3", "192"], any) and not checkName(["flac"], all): if checkName(["mp3", "192"], any) and not checkName(["flac"], all):
return Quality.B192 return Quality.B192
elif checkName(["mp3", "256"], any) and not checkName(["flac"], all): elif checkName(["mp3", "256"], any) and not checkName(["flac"], all):
return Quality.B256 return Quality.B256
elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all): elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all):
return Quality.VBR return Quality.VBR
elif checkName(["mp3", "320"], any) and not checkName(["flac"], all): elif checkName(["mp3", "320"], any) and not checkName(["flac"], all):
return Quality.B320 return Quality.B320
else: else:
return Quality.UNKNOWN return Quality.UNKNOWN
@staticmethod @staticmethod
def assumeQuality(name): def assumeQuality(name):
if name.lower().endswith(".mp3"): if name.lower().endswith(".mp3"):
return Quality.MP3 return Quality.MP3
elif name.lower().endswith(".flac"): elif name.lower().endswith(".flac"):
return Quality.LOSSLESS return Quality.LOSSLESS
else: else:
return Quality.UNKNOWN return Quality.UNKNOWN
@staticmethod @staticmethod
def compositeStatus(status, quality): def compositeStatus(status, quality):
return status + 100 * quality return status + 100 * quality
@staticmethod @staticmethod
def qualityDownloaded(status): def qualityDownloaded(status):
return (status - DOWNLOADED) / 100 return (status - DOWNLOADED) / 100
@staticmethod @staticmethod
def splitCompositeStatus(status): def splitCompositeStatus(status):
"""Returns a tuple containing (status, quality)""" """Returns a tuple containing (status, quality)"""
for x in sorted(Quality.qualityStrings.keys(), reverse=True): for x in sorted(Quality.qualityStrings.keys(), reverse=True):
if status > x*100: if status > x*100:
return (status-x*100, x) return (status-x*100, x)
return (Quality.NONE, status) return (Quality.NONE, status)
@staticmethod @staticmethod
def statusFromName(name, assume=True): def statusFromName(name, assume=True):
quality = Quality.nameQuality(name) quality = Quality.nameQuality(name)
if assume and quality == Quality.UNKNOWN: if assume and quality == Quality.UNKNOWN:
quality = Quality.assumeQuality(name) quality = Quality.assumeQuality(name)
return Quality.compositeStatus(DOWNLOADED, quality) return Quality.compositeStatus(DOWNLOADED, quality)
DOWNLOADED = None DOWNLOADED = None
SNATCHED = None SNATCHED = None
SNATCHED_PROPER = None SNATCHED_PROPER = None
Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()] Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()]
Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()] Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()]
Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()] Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()]
MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], []) MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], [])
LOSSLESS = Quality.combineQualities([Quality.FLAC], []) LOSSLESS = Quality.combineQualities([Quality.FLAC], [])
ANY = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], []) ANY = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], [])
qualityPresets = (MP3, LOSSLESS, ANY) qualityPresets = (MP3, LOSSLESS, ANY)
qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)", qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)",
LOSSLESS: "Lossless (flac)", LOSSLESS: "Lossless (flac)",
ANY: "Any"} ANY: "Any"}
+29 -34
View File
@@ -31,7 +31,7 @@ from headphones import logger
def dbFilename(filename="headphones.db"): def dbFilename(filename="headphones.db"):
return os.path.join(headphones.DATA_DIR, filename) return os.path.join(headphones.DATA_DIR, filename)
def getCacheSize(): def getCacheSize():
#this will protect against typecasting problems produced by empty string and None settings #this will protect against typecasting problems produced by empty string and None settings
if not headphones.CACHE_SIZEMB: if not headphones.CACHE_SIZEMB:
@@ -42,68 +42,63 @@ def getCacheSize():
class DBConnection: class DBConnection:
def __init__(self, filename="headphones.db"): def __init__(self, filename="headphones.db"):
self.filename = filename self.filename = filename
self.connection = sqlite3.connect(dbFilename(filename), timeout=20) self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
#don't wait for the disk to finish writing #don't wait for the disk to finish writing
self.connection.execute("PRAGMA synchronous = OFF") self.connection.execute("PRAGMA synchronous = OFF")
#journal disabled since we never do rollbacks #journal disabled since we never do rollbacks
self.connection.execute("PRAGMA journal_mode = %s" % headphones.JOURNAL_MODE) self.connection.execute("PRAGMA journal_mode = %s" % headphones.JOURNAL_MODE)
#64mb of cache memory,probably need to make it user configurable #64mb of cache memory,probably need to make it user configurable
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize()*1024)) self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize()*1024))
self.connection.row_factory = sqlite3.Row self.connection.row_factory = sqlite3.Row
def action(self, query, args=None): def action(self, query, args=None):
if query == None: if query == None:
return return
sqlResult = None sqlResult = None
attempt = 0
while attempt < 5: try:
try: with self.connection as c:
if args == None: if args == None:
#logger.debug(self.filename+": "+query) sqlResult = c.execute(query)
sqlResult = self.connection.execute(query)
else: else:
#logger.debug(self.filename+": "+query+" with args "+str(args)) sqlResult = c.execute(query, args)
sqlResult = self.connection.execute(query, args)
self.connection.commit() except sqlite3.OperationalError, e:
break if "unable to open database file" in e.message or "database is locked" in e.message:
except sqlite3.OperationalError, e: logger.warn('Database Error: %s', e)
if "unable to open database file" in e.message or "database is locked" in e.message: else:
logger.warn('Database Error: %s', e) logger.error('Database error: %s', e)
attempt += 1
time.sleep(1)
else:
logger.error('Database error: %s', e)
raise
except sqlite3.DatabaseError, e:
logger.error('Fatal Error executing %s :: %s', query, e)
raise raise
except sqlite3.DatabaseError, e:
logger.error('Fatal Error executing %s :: %s', query, e)
raise
return sqlResult return sqlResult
def select(self, query, args=None): def select(self, query, args=None):
sqlResults = self.action(query, args).fetchall() sqlResults = self.action(query, args).fetchall()
if sqlResults == None: if sqlResults == None or sqlResults == [None]:
return [] return []
return sqlResults return sqlResults
def upsert(self, tableName, valueDict, keyDict): def upsert(self, tableName, valueDict, keyDict):
changesBefore = self.connection.total_changes changesBefore = self.connection.total_changes
genParams = lambda myDict : [x + " = ?" for x in myDict.keys()] genParams = lambda myDict : [x + " = ?" for x in myDict.keys()]
query = "UPDATE "+tableName+" SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict)) query = "UPDATE "+tableName+" SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
self.action(query, valueDict.values() + keyDict.values()) self.action(query, valueDict.values() + keyDict.values())
if self.connection.total_changes == changesBefore: if self.connection.total_changes == changesBefore:
query = "INSERT INTO "+tableName+" (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + \ query = "INSERT INTO "+tableName+" (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + \
" VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")" " VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")"
+24 -41
View File
@@ -1,41 +1,24 @@
# This file is part of Headphones. # This file is part of Headphones.
# #
# Headphones is free software: you can redistribute it and/or modify # Headphones is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# Headphones is distributed in the hope that it will be useful, # Headphones is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
def ex(e): class HeadphonesException(Exception):
""" """
Returns a string from the exception text if it exists. Generic Headphones Exception - should never be thrown, only subclassed
""" """
# sanity check class NewzbinAPIThrottled(HeadphonesException):
if not e.args or not e.args[0]: """
return "" Newzbin has throttled us, deal with it
"""
e_message = e.args[0]
# if fixStupidEncodings doesn't fix it then maybe it's not a string, in which case we'll try printing it anyway
if not e_message:
try:
e_message = str(e.args[0])
except:
e_message = ""
return e_message
class HeadphonesException(Exception):
"Generic Headphones Exception - should never be thrown, only subclassed"
class NewzbinAPIThrottled(HeadphonesException):
"Newzbin has throttled us, deal with it"
+2 -2
View File
@@ -8,7 +8,7 @@ from headphones import logger
def getXldProfile(xldProfile): def getXldProfile(xldProfile):
xldProfileNotFound = xldProfile xldProfileNotFound = xldProfile
expandedPath = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist') expandedPath = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist')
try: try:
preferences = plistlib.Plist.fromFile(expandedPath) preferences = plistlib.Plist.fromFile(expandedPath)
except (expat.ExpatError): except (expat.ExpatError):
os.system("/usr/bin/plutil -convert xml1 %s" % expandedPath ) os.system("/usr/bin/plutil -convert xml1 %s" % expandedPath )
@@ -61,7 +61,7 @@ def getXldProfile(xldProfile):
elif 'TVBR' in ShortDesc: elif 'TVBR' in ShortDesc:
XLDAacOutput2_VBRQuality = int(profile.get('XLDAacOutput2_VBRQuality')) XLDAacOutput2_VBRQuality = int(profile.get('XLDAacOutput2_VBRQuality'))
if XLDAacOutput2_VBRQuality > 122: if XLDAacOutput2_VBRQuality > 122:
xldBitrate = 320 xldBitrate = 320
elif XLDAacOutput2_VBRQuality > 113 and XLDAacOutput2_VBRQuality <= 122: elif XLDAacOutput2_VBRQuality > 113 and XLDAacOutput2_VBRQuality <= 122:
xldBitrate = 285 xldBitrate = 285
elif XLDAacOutput2_VBRQuality > 104 and XLDAacOutput2_VBRQuality <= 113: elif XLDAacOutput2_VBRQuality > 104 and XLDAacOutput2_VBRQuality <= 113:
+24 -15
View File
@@ -19,6 +19,8 @@ import time
import shutil import shutil
import datetime import datetime
import headphones import headphones
import unicodedata
import sys
from operator import itemgetter from operator import itemgetter
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
@@ -168,12 +170,17 @@ def piratesize(size):
return size return size
def replace_all(text, dic): def replace_all(text, dic, normalize=False):
if not text: if not text:
return '' return ''
for i, j in dic.iteritems(): for i, j in dic.iteritems():
if normalize:
if sys.platform == 'darwin':
j = unicodedata.normalize('NFD', j)
else:
j = unicodedata.normalize('NFC', j)
text = text.replace(i, j) text = text.replace(i, j)
return text return text
@@ -274,7 +281,7 @@ def expand_subfolders(f):
difference = max(path_depths) - min(path_depths) difference = max(path_depths) - min(path_depths)
if difference > 0: if difference > 0:
logger.info("Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder" % (len(media_folders), difference)) logger.info("Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder.", len(media_folders), difference)
# While already failed, advice the user what he could try. We assume the # While already failed, advice the user what he could try. We assume the
# directory may contain separate CD's and maybe some extra's. The # directory may contain separate CD's and maybe some extra's. The
@@ -283,7 +290,7 @@ def expand_subfolders(f):
extra_media_folders = [ media_folder[:min(path_depths)] for media_folder in media_folders if len(media_folder) > min(path_depths) ] extra_media_folders = [ media_folder[:min(path_depths)] for media_folder in media_folders if len(media_folder) > min(path_depths) ]
extra_media_folders = list(set([ os.path.join(*media_folder) for media_folder in extra_media_folders ])) extra_media_folders = list(set([ os.path.join(*media_folder) for media_folder in extra_media_folders ]))
logger.info("Please look at the following folder(s), since they cause the depth difference: %s" % extra_media_folders) logger.info("Please look at the following folder(s), since they cause the depth difference: %s", extra_media_folders)
return return
# Convert back to paths and remove duplicates, which may be there after # Convert back to paths and remove duplicates, which may be there after
@@ -297,7 +304,7 @@ def expand_subfolders(f):
logger.debug("Did not expand subfolder, as it resulted in one folder.") logger.debug("Did not expand subfolder, as it resulted in one folder.")
return return
logger.debug("Expanded subfolders in folder: " % media_folders) logger.debug("Expanded subfolders in folder: %s", media_folders)
return media_folders return media_folders
def extract_data(s): def extract_data(s):
@@ -360,7 +367,7 @@ def extract_metadata(f):
# Try to read the file info # Try to read the file info
try: try:
media_file = MediaFile(os.path.join(root, file)) media_file = MediaFile(os.path.join(root, file))
except FileTypeError, UnreadableFileError: except (FileTypeError, UnreadableFileError):
# Probably not a media file # Probably not a media file
continue continue
@@ -374,14 +381,14 @@ def extract_metadata(f):
# Verify results # Verify results
if len(results) == 0: if len(results) == 0:
logger.info("No metadata in media files found, ignoring") logger.info("No metadata in media files found, ignoring.")
return (None, None, None) return (None, None, None)
# Require that some percentage of files have tags # Require that some percentage of files have tags
count_ratio = 0.75 count_ratio = 0.75
if count < (count_ratio * len(results)): if count < (count_ratio * len(results)):
logger.info("Counted %d media files, but only %d have tags, ignoring" % (count, len(results))) logger.info("Counted %d media files, but only %d have tags, ignoring.", count, len(results))
return (None, None, None) return (None, None, None)
# Count distinct values # Count distinct values
@@ -399,7 +406,7 @@ def extract_metadata(f):
old_album = new_albums[index] old_album = new_albums[index]
new_albums[index] = RE_CD_ALBUM.sub("", album).strip() new_albums[index] = RE_CD_ALBUM.sub("", album).strip()
logger.debug("Stripped albumd number identifier: %s -> %s" % (old_album, new_albums[index])) logger.debug("Stripped albumd number identifier: %s -> %s", old_album, new_albums[index])
# Remove duplicates # Remove duplicates
new_albums = list(set(new_albums)) new_albums = list(set(new_albums))
@@ -417,7 +424,7 @@ def extract_metadata(f):
if len(artists) > 1 and len(albums) == 1: if len(artists) > 1 and len(albums) == 1:
split_artists = [ RE_FEATURING.split(artist) for artist in artists ] split_artists = [ RE_FEATURING.split(artist) for artist in artists ]
featurings = [ len(split_artist) - 1 for split_artist in split_artists ] featurings = [ len(split_artist) - 1 for split_artist in split_artists ]
logger.info("Album seem to feature %d different artists" % sum(featurings)) logger.info("Album seem to feature %d different artists", sum(featurings))
if sum(featurings) > 0: if sum(featurings) > 0:
# Find the artist of which the least splits have been generated. # Find the artist of which the least splits have been generated.
@@ -429,8 +436,8 @@ def extract_metadata(f):
return (artist, albums[0], years[0]) return (artist, albums[0], years[0])
# Not sure what to do here. # Not sure what to do here.
logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring" % (len(artists), len(albums), len(years))) logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring", len(artists), len(albums), len(years))
logger.debug("Artists: %s, Albums: %s, Years: %s" % (artists, albums, years)) logger.debug("Artists: %s, Albums: %s, Years: %s", artists, albums, years)
return (None, None, None) return (None, None, None)
@@ -486,7 +493,7 @@ def smartMove(src, dest, delete=True):
filename = os.path.basename(src) filename = os.path.basename(src)
if os.path.isfile(os.path.join(dest, filename)): if os.path.isfile(os.path.join(dest, filename)):
logger.info('Destination file exists: %s', os.path.join(dest, filename).decode(headphones.SYS_ENCODING, 'replace')) logger.info('Destination file exists: %s', os.path.join(dest, filename))
title = os.path.splitext(filename)[0] title = os.path.splitext(filename)[0]
ext = os.path.splitext(filename)[1] ext = os.path.splitext(filename)[1]
i = 1 i = 1
@@ -596,9 +603,11 @@ def create_https_certificates(ssl_cert, ssl_key):
# Save the key and certificate to disk # Save the key and certificate to disk
try: try:
open(ssl_key, 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) with open(ssl_key, 'w') as f:
open(ssl_cert, 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
except Exception, e: with open(ssl_cert, 'w') as f:
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
except IOError as e:
logger.error("Error creating SSL key and certificate: %s", e) logger.error("Error creating SSL key and certificate: %s", e)
return False return False
+185 -125
View File
@@ -13,24 +13,26 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from lib.pyItunes import *
import time
import threading
import os
from beets.mediafile import MediaFile
import headphones
from headphones import logger, helpers, db, mb, lastfm from headphones import logger, helpers, db, mb, lastfm
blacklisted_special_artist_names = ['[anonymous]','[data]','[no artist]','[traditional]','[unknown]','Various Artists'] from beets.mediafile import MediaFile
blacklisted_special_artists = ['f731ccc4-e22a-43af-a747-64213329e088','33cf029c-63b0-41a0-9855-be2a3665fb3b',\
'314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc','eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61',\
'9be7f096-97ec-4615-8957-8d40b5dcbc41','125ec42a-7229-4250-afc5-e057484327fe',\
'89ad4ac3-39f7-470e-963a-56509c546377']
import os
import time
import threading
import headphones
blacklisted_special_artist_names = ['[anonymous]', '[data]', '[no artist]',
'[traditional]','[unknown]','Various Artists']
blacklisted_special_artists = ['f731ccc4-e22a-43af-a747-64213329e088',
'33cf029c-63b0-41a0-9855-be2a3665fb3b',
'314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc',
'eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61',
'9be7f096-97ec-4615-8957-8d40b5dcbc41',
'125ec42a-7229-4250-afc5-e057484327fe',
'89ad4ac3-39f7-470e-963a-56509c546377']
def is_exists(artistid): def is_exists(artistid):
myDB = db.DBConnection() myDB = db.DBConnection()
# See if the artist is already in the database # See if the artist is already in the database
@@ -56,7 +58,7 @@ def artistlist_to_mbids(artistlist, forced=False):
if not isinstance(artist, unicode): if not isinstance(artist, unicode):
try: try:
artist = artist.decode('utf-8', 'replace') artist = artist.decode('utf-8', 'replace')
except: except Exception:
logger.warn("Unable to convert artist to unicode so cannot do a database lookup") logger.warn("Unable to convert artist to unicode so cannot do a database lookup")
continue continue
@@ -68,7 +70,6 @@ def artistlist_to_mbids(artistlist, forced=False):
try: try:
artistid = results[0]['id'] artistid = results[0]['id']
except IndexError: except IndexError:
logger.info('MusicBrainz query turned up no matches for: %s' % artist) logger.info('MusicBrainz query turned up no matches for: %s' % artist)
continue continue
@@ -98,9 +99,10 @@ def artistlist_to_mbids(artistlist, forced=False):
# Update the similar artist tag cloud: # Update the similar artist tag cloud:
logger.info('Updating artist information from Last.fm') logger.info('Updating artist information from Last.fm')
try: try:
lastfm.getSimilar() lastfm.getSimilar()
except Exception, e: except Exception as e:
logger.warn('Failed to update arist information from Last.fm: %s' % e) logger.warn('Failed to update arist information from Last.fm: %s' % e)
def addArtistIDListToDB(artistidlist): def addArtistIDListToDB(artistidlist):
@@ -129,11 +131,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
# We need the current minimal info in the database instantly # We need the current minimal info in the database instantly
# so we don't throw a 500 error when we redirect to the artistPage # so we don't throw a 500 error when we redirect to the artistPage
controlValueDict = {"ArtistID": artistid} controlValueDict = {"ArtistID": artistid}
# Don't replace a known artist name with an "Artist ID" placeholder # Don't replace a known artist name with an "Artist ID" placeholder
dbartist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [artistid]).fetchone() dbartist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [artistid]).fetchone()
# Only modify the Include Extras stuff if it's a new artist. We need it early so we know what to fetch # Only modify the Include Extras stuff if it's a new artist. We need it early so we know what to fetch
@@ -182,24 +182,27 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
myDB.upsert("artists", newValueDict, controlValueDict) myDB.upsert("artists", newValueDict, controlValueDict)
# See if we need to grab extras. Artist specific extras take precedence over global option # See if we need to grab extras. Artist specific extras take precedence
# Global options are set when adding a new artist # over global option. Global options are set when adding a new artist
myDB = db.DBConnection()
try: try:
db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?', [artistid]).fetchone() db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?', [artistid]).fetchone()
includeExtras = db_artist['IncludeExtras'] includeExtras = db_artist['IncludeExtras']
except IndexError: except IndexError:
includeExtras = False includeExtras = False
#Clean all references to release group in dB that are no longer referenced in musicbrainz # Clean all references to release group in dB that are no longer referenced
# from the musicbrainz refresh
group_list = [] group_list = []
force_repackage = 0 force_repackage = 0
#Don't nuke the database if there's a MusicBrainz error
if len(artist['releasegroups']) != 0 and not extrasonly: # Don't nuke the database if there's a MusicBrainz error
if len(artist['releasegroups']) != 0:
for groups in artist['releasegroups']: for groups in artist['releasegroups']:
group_list.append(groups['id']) group_list.append(groups['id'])
remove_missing_groups_from_albums = myDB.action("SELECT AlbumID FROM albums WHERE ArtistID=?", [artistid]) if not extrasonly:
remove_missing_groups_from_albums = myDB.select("SELECT AlbumID FROM albums WHERE ArtistID=?", [artistid])
else:
remove_missing_groups_from_albums = myDB.select('SELECT AlbumID FROM albums WHERE ArtistID=? AND Status="Skipped" AND Type!="Album"', [artistid])
for items in remove_missing_groups_from_albums: for items in remove_missing_groups_from_albums:
if items['AlbumID'] not in group_list: if items['AlbumID'] not in group_list:
# Remove all from albums/tracks that aren't in release groups # Remove all from albums/tracks that aren't in release groups
@@ -208,10 +211,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
myDB.action("DELETE FROM tracks WHERE AlbumID=?", [items['AlbumID']]) myDB.action("DELETE FROM tracks WHERE AlbumID=?", [items['AlbumID']])
myDB.action("DELETE FROM alltracks WHERE AlbumID=?", [items['AlbumID']]) myDB.action("DELETE FROM alltracks WHERE AlbumID=?", [items['AlbumID']])
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [items['AlbumID']]) myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [items['AlbumID']])
logger.info("[%s] Removing all references to release group %s to reflect MusicBrainz" % (artist['artist_name'], items['AlbumID'])) logger.info("[%s] Removing all references to release group %s to reflect MusicBrainz refresh" % (artist['artist_name'], items['AlbumID']))
force_repackage = 1 if not extrasonly:
force_repackage = 1
else: else:
logger.info("[%s] There was either an error pulling data from MusicBrainz or there might not be any releases for this category" % artist['artist_name']) if not extrasonly:
logger.info("[%s] There was either an error pulling data from MusicBrainz or there might not be any releases for this category" % artist['artist_name'])
# Then search for releases within releasegroups, if releases don't exist, then remove from allalbums/alltracks # Then search for releases within releasegroups, if releases don't exist, then remove from allalbums/alltracks
album_searches = [] album_searches = []
@@ -224,63 +229,65 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
#Make a user configurable variable to skip update of albums with release dates older than this date (in days) #Make a user configurable variable to skip update of albums with release dates older than this date (in days)
pause_delta = headphones.MB_IGNORE_AGE pause_delta = headphones.MB_IGNORE_AGE
check_release_date = myDB.action("SELECT ReleaseDate, Status from albums WHERE ArtistID=? AND AlbumTitle=?", (artistid, al_title)).fetchone() rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
#Skip update if Status set
if check_release_date and check_release_date[1]:
logger.info("[%s] Not updating: %s (Status is %s, skipping)" % (artist['artist_name'], rg['title'], check_release_date[1]))
continue
if not forcefull: if not forcefull:
if check_release_date: new_release_group = False
if check_release_date[0] is None:
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title'])) try:
new_releases = mb.get_new_releases(rgid,includeExtras,True) check_release_date = rg_exists['ReleaseDate']
else: except TypeError:
if len(check_release_date[0]) == 10: check_release_date = None
release_date = check_release_date[0] new_release_group = True
elif len(check_release_date[0]) == 7:
release_date = check_release_date[0]+"-31"
elif len(check_release_date[0]) == 4: if new_release_group:
release_date = check_release_date[0]+"-12-31"
else:
release_date = today
if helpers.get_age(today) - helpers.get_age(release_date) < pause_delta:
logger.info("[%s] Now updating: %s (Release Date <%s Days) " % (artist['artist_name'], rg['title'], pause_delta))
new_releases = mb.get_new_releases(rgid,includeExtras,True)
else:
logger.info("[%s] Skipping: %s (Release Date >%s Days)" % (artist['artist_name'], rg['title'], pause_delta))
skip_log = 1
new_releases = 0
else:
logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title'])) logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid,includeExtras) new_releases = mb.get_new_releases(rgid,includeExtras)
else:
if check_release_date is None or check_release_date == u"None":
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid,includeExtras,True)
else:
if len(check_release_date) == 10:
release_date = check_release_date
elif len(check_release_date) == 7:
release_date = check_release_date+"-31"
elif len(check_release_date) == 4:
release_date = check_release_date+"-12-31"
else:
release_date = today
if helpers.get_age(today) - helpers.get_age(release_date) < pause_delta:
logger.info("[%s] Now updating: %s (Release Date <%s Days)", artist['artist_name'], rg['title'], pause_delta)
new_releases = mb.get_new_releases(rgid,includeExtras,True)
else:
logger.info("[%s] Skipping: %s (Release Date >%s Days)", artist['artist_name'], rg['title'], pause_delta)
skip_log = 1
new_releases = 0
if force_repackage == 1: if force_repackage == 1:
new_releases = -1 new_releases = -1
logger.info('[%s] Forcing repackage of %s (Release Group Removed)' % (artist['artist_name'], al_title)) logger.info('[%s] Forcing repackage of %s (Release Group Removed)', artist['artist_name'], al_title)
else: else:
new_releases = new_releases new_releases = new_releases
else: else:
logger.info("[%s] Now adding/updating: %s (Comprehensive Force)" % (artist['artist_name'], rg['title'])) logger.info("[%s] Now adding/updating: %s (Comprehensive Force)", artist['artist_name'], rg['title'])
new_releases = mb.get_new_releases(rgid,includeExtras,forcefull) new_releases = mb.get_new_releases(rgid,includeExtras,forcefull)
#What this does is adds new releases per artist to the allalbums + alltracks databases
#new_releases = mb.get_new_releases(rgid,includeExtras)
#print al_title
#print new_releases
if new_releases != 0: if new_releases != 0:
#Dump existing hybrid release since we're repackaging/replacing it # Dump existing hybrid release since we're repackaging/replacing it
myDB.action("DELETE from albums WHERE ReleaseID=?", [rg['id']]) myDB.action("DELETE from albums WHERE ReleaseID=?", [rg['id']])
myDB.action("DELETE from allalbums WHERE ReleaseID=?", [rg['id']]) myDB.action("DELETE from allalbums WHERE ReleaseID=?", [rg['id']])
myDB.action("DELETE from tracks WHERE ReleaseID=?", [rg['id']]) myDB.action("DELETE from tracks WHERE ReleaseID=?", [rg['id']])
myDB.action("DELETE from alltracks WHERE ReleaseID=?", [rg['id']]) myDB.action("DELETE from alltracks WHERE ReleaseID=?", [rg['id']])
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rg['id']]) myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [rg['id']])
# This will be used later to build a hybrid release # This will be used later to build a hybrid release
fullreleaselist = [] fullreleaselist = []
#Search for releases within a release group # Search for releases within a release group
find_hybrid_releases = myDB.action("SELECT * from allalbums WHERE AlbumID=?", [rg['id']]) find_hybrid_releases = myDB.action("SELECT * from allalbums WHERE AlbumID=?", [rg['id']])
# Build the dictionary for the fullreleaselist # Build the dictionary for the fullreleaselist
for items in find_hybrid_releases: for items in find_hybrid_releases:
if items['ReleaseID'] != rg['id']: #don't include hybrid information, since that's what we're replacing if items['ReleaseID'] != rg['id']: #don't include hybrid information, since that's what we're replacing
@@ -311,14 +318,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
newValueDict['Tracks'] = hybrid_track_array newValueDict['Tracks'] = hybrid_track_array
fullreleaselist.append(newValueDict) fullreleaselist.append(newValueDict)
#print fullreleaselist
# Basically just do the same thing again for the hybrid release # Basically just do the same thing again for the hybrid release
# This may end up being called with an empty fullreleaselist # This may end up being called with an empty fullreleaselist
try: try:
hybridrelease = getHybridRelease(fullreleaselist) hybridrelease = getHybridRelease(fullreleaselist)
logger.info('[%s] Packaging %s releases into hybrid title' % (artist['artist_name'], rg['title'])) logger.info('[%s] Packaging %s releases into hybrid title' % (artist['artist_name'], rg['title']))
except Exception, e: except Exception as e:
errors = True errors = True
logger.warn('[%s] Unable to get hybrid release information for %s: %s' % (artist['artist_name'],rg['title'],e)) logger.warn('[%s] Unable to get hybrid release information for %s: %s' % (artist['artist_name'],rg['title'],e))
continue continue
@@ -378,11 +383,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
# If there's no release in the main albums tables, add the default (hybrid) # If there's no release in the main albums tables, add the default (hybrid)
# If there is a release, check the ReleaseID against the AlbumID to see if they differ (user updated) # If there is a release, check the ReleaseID against the AlbumID to see if they differ (user updated)
# check if the album already exists # check if the album already exists
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
if not rg_exists: if not rg_exists:
releaseid = rg['id'] releaseid = rg['id']
else: else:
releaseid = rg_exists['ReleaseID'] releaseid = rg_exists['ReleaseID']
if not releaseid:
releaseid = rg['id']
album = myDB.action('SELECT * from allalbums WHERE ReleaseID=?', [releaseid]).fetchone() album = myDB.action('SELECT * from allalbums WHERE ReleaseID=?', [releaseid]).fetchone()
@@ -399,11 +405,14 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
"ReleaseFormat": album['ReleaseFormat'] "ReleaseFormat": album['ReleaseFormat']
} }
if not rg_exists: if rg_exists:
newValueDict['DateAdded'] = rg_exists['DateAdded']
newValueDict['Status'] = rg_exists['Status']
else:
today = helpers.today() today = helpers.today()
newValueDict['DateAdded']= today newValueDict['DateAdded'] = today
if headphones.AUTOWANT_ALL: if headphones.AUTOWANT_ALL:
newValueDict['Status'] = "Wanted" newValueDict['Status'] = "Wanted"
@@ -420,11 +429,17 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
tracks = myDB.action('SELECT * from alltracks WHERE ReleaseID=?', [releaseid]).fetchall() tracks = myDB.action('SELECT * from alltracks WHERE ReleaseID=?', [releaseid]).fetchall()
# This is used to see how many tracks you have from an album - to mark it as downloaded. Default is 80%, can be set in config as ALBUM_COMPLETION_PCT # This is used to see how many tracks you have from an album - to
# mark it as downloaded. Default is 80%, can be set in config as
# ALBUM_COMPLETION_PCT
total_track_count = len(tracks) total_track_count = len(tracks)
for track in tracks: if total_track_count == 0:
logger.warning("Total track count is zero for Release ID " +
"'%s', skipping.", releaseid)
continue
for track in tracks:
controlValueDict = {"TrackID": track['TrackID'], controlValueDict = {"TrackID": track['TrackID'],
"AlbumID": rg['id']} "AlbumID": rg['id']}
@@ -469,10 +484,35 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
if skip_log == 0: if skip_log == 0:
logger.info(u"[%s] No new releases, so no changes made to %s" % (artist['artist_name'], rg['title'])) logger.info(u"[%s] No new releases, so no changes made to %s" % (artist['artist_name'], rg['title']))
time.sleep(3)
finalize_update(artistid, artist['artist_name'], errors)
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
cache.getThumb(ArtistID=artistid)
if errors:
logger.info("[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (artist['artist_name'], artist['artist_name']))
else:
myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']])
logger.info(u"Updating complete for: %s" % artist['artist_name'])
# Start searching for newly added albums
if album_searches:
from headphones import searcher
logger.info("Start searching for %d albums.", len(album_searches))
for album_search in album_searches:
searcher.searchforalbum(albumid=album_search)
def finalize_update(artistid, artistname, errors=False):
# Moving this little bit to it's own function so we can update have tracks & latest album when deleting extras
myDB = db.DBConnection()
latestalbum = myDB.action('SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC', [artistid]).fetchone() latestalbum = myDB.action('SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC', [artistid]).fetchone()
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [artistid]))
#havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['artist_name']])) #havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['artist_name']]))
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist['artist_name']])) havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artistname]))
controlValueDict = {"ArtistID": artistid} controlValueDict = {"ArtistID": artistid}
@@ -493,27 +533,23 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
myDB.upsert("artists", newValueDict, controlValueDict) myDB.upsert("artists", newValueDict, controlValueDict)
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name']) def addReleaseById(rid, rgid=None):
cache.getThumb(ArtistID=artistid)
if errors:
logger.info("[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (artist['artist_name'], artist['artist_name']))
else:
myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']])
logger.info(u"Updating complete for: %s" % artist['artist_name'])
# Start searching for newly added albums
if album_searches:
from headphones import searcher
logger.info("Start searching for %d albums.", len(album_searches))
for album_search in album_searches:
searcher.searchforalbum(albumid=album_search)
def addReleaseById(rid):
myDB = db.DBConnection() myDB = db.DBConnection()
# Create minimum info upfront if added from searchresults
status = ''
if rgid:
dbalbum = myDB.select("SELECT * from albums WHERE AlbumID=?", [rgid])
if not dbalbum:
status = 'Loading'
controlValueDict = {"AlbumID": rgid}
newValueDict = {"AlbumTitle": rgid,
"ArtistName": status,
"Status": status}
myDB.upsert("albums", newValueDict, controlValueDict)
time.sleep(1)
rgid = None rgid = None
artistid = None artistid = None
release_dict = None release_dict = None
@@ -527,11 +563,15 @@ def addReleaseById(rid):
logger.debug("Didn't find releaseID " + rid + " in the cache. Looking up its ReleaseGroupID") logger.debug("Didn't find releaseID " + rid + " in the cache. Looking up its ReleaseGroupID")
try: try:
release_dict = mb.getRelease(rid) release_dict = mb.getRelease(rid)
except Exception, e: except Exception as e:
logger.info('Unable to get release information for Release %s: %s', rid, e) logger.info('Unable to get release information for Release %s: %s', rid, e)
if status == 'Loading':
myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid])
return return
if not release_dict: if not release_dict:
logger.info('Unable to get release information for Release %s: no dict', rid) logger.info('Unable to get release information for Release %s: no dict', rid)
if status == 'Loading':
myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid])
return return
rgid = release_dict['rgid'] rgid = release_dict['rgid']
@@ -549,7 +589,6 @@ def addReleaseById(rid):
else: else:
sortname = release_dict['artist_name'] sortname = release_dict['artist_name']
logger.info(u"Now manually adding: " + release_dict['artist_name'] + " - with status Paused") logger.info(u"Now manually adding: " + release_dict['artist_name'] + " - with status Paused")
controlValueDict = {"ArtistID": release_dict['artist_id']} controlValueDict = {"ArtistID": release_dict['artist_id']}
newValueDict = {"ArtistName": release_dict['artist_name'], newValueDict = {"ArtistName": release_dict['artist_name'],
@@ -565,21 +604,27 @@ def addReleaseById(rid):
elif not artist_exists and not release_dict: elif not artist_exists and not release_dict:
logger.error("Artist does not exist in the database and did not get a valid response from MB. Skipping release.") logger.error("Artist does not exist in the database and did not get a valid response from MB. Skipping release.")
if status == 'Loading':
myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid])
return return
if not rg_exists and release_dict: #it should never be the case that we have an rg and not the artist if not rg_exists and release_dict or status == 'Loading' and release_dict: #it should never be the case that we have an rg and not the artist
#but if it is this will fail #but if it is this will fail
logger.info(u"Now adding-by-id album (" + release_dict['title'] + ") from id: " + rgid) logger.info(u"Now adding-by-id album (" + release_dict['title'] + ") from id: " + rgid)
controlValueDict = {"AlbumID": rgid} controlValueDict = {"AlbumID": rgid}
if status != 'Loading':
status = 'Wanted'
newValueDict = {"ArtistID": release_dict['artist_id'], newValueDict = {"ArtistID": release_dict['artist_id'],
"ReleaseID": rgid,
"ArtistName": release_dict['artist_name'], "ArtistName": release_dict['artist_name'],
"AlbumTitle": release_dict['rg_title'], "AlbumTitle": release_dict['rg_title'],
"AlbumASIN": release_dict['asin'], "AlbumASIN": release_dict['asin'],
"ReleaseDate": release_dict['date'], "ReleaseDate": release_dict['date'],
"DateAdded": helpers.today(), "DateAdded": helpers.today(),
"Status": 'Wanted', "Status": status,
"Type": release_dict['rg_type'] "Type": release_dict['rg_type'],
"ReleaseID": rid
} }
myDB.upsert("albums", newValueDict, controlValueDict) myDB.upsert("albums", newValueDict, controlValueDict)
@@ -588,7 +633,6 @@ def addReleaseById(rid):
myDB.action('INSERT INTO releases VALUES( ?, ?)', [rid, release_dict['rgid']]) myDB.action('INSERT INTO releases VALUES( ?, ?)', [rid, release_dict['rgid']])
for track in release_dict['tracks']: for track in release_dict['tracks']:
cleanname = helpers.cleanName(release_dict['artist_name'] + ' ' + release_dict['rg_title'] + ' ' + track['title']) cleanname = helpers.cleanName(release_dict['artist_name'] + ' ' + release_dict['rg_title'] + ' ' + track['title'])
controlValueDict = {"TrackID": track['id'], controlValueDict = {"TrackID": track['id'],
@@ -619,11 +663,19 @@ def addReleaseById(rid):
myDB.upsert("tracks", newValueDict, controlValueDict) myDB.upsert("tracks", newValueDict, controlValueDict)
#start a search for the album # Reset status
if status == 'Loading':
controlValueDict = {"AlbumID": rgid}
newValueDict = {"Status": "Wanted"}
myDB.upsert("albums", newValueDict, controlValueDict)
# Start a search for the album
import searcher import searcher
searcher.searchforalbum(rgid, False) searcher.searchforalbum(rgid, False)
elif not rg_exists and not release_dict: elif not rg_exists and not release_dict:
logger.error("ReleaseGroup does not exist in the database and did not get a valid response from MB. Skipping release.") logger.error("ReleaseGroup does not exist in the database and did not get a valid response from MB. Skipping release.")
if status == 'Loading':
myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid])
return return
else: else:
logger.info('Release ' + str(rid) + " already exists in the database!") logger.info('Release ' + str(rid) + " already exists in the database!")
@@ -659,39 +711,43 @@ def updateFormat():
def getHybridRelease(fullreleaselist): def getHybridRelease(fullreleaselist):
""" """
Returns a dictionary of best group of tracks from the list of releases & earliest release date Returns a dictionary of best group of tracks from the list of releases and
earliest release date
""" """
if len(fullreleaselist) == 0: if len(fullreleaselist) == 0:
raise Exception("getHybridRelease was called with an empty fullreleaselist") raise ValueError("Empty fullreleaselist")
sortable_release_list = [] sortable_release_list = []
formats = {
'2xVinyl': '2',
'Vinyl': '2',
'CD': '0',
'Cassette': '3',
'2xCD': '1',
'Digital Media': '0'
}
countries = {
'US': '0',
'GB': '1',
'JP': '2',
}
for release in fullreleaselist: for release in fullreleaselist:
# Find values for format and country
formats = {
'2xVinyl': '2',
'Vinyl': '2',
'CD': '0',
'Cassette': '3',
'2xCD': '1',
'Digital Media': '0'
}
countries = {
'US': '0',
'GB': '1',
'JP': '2',
}
try: try:
format = int(formats[release['Format']]) format = int(formats[release['Format']])
except: except (ValueError, KeyError):
format = 3 format = 3
try: try:
country = int(countries[release['Country']]) country = int(countries[release['Country']])
except: except (ValueError, KeyError):
country = 3 country = 3
# Create record
release_dict = { release_dict = {
'hasasin': bool(release['AlbumASIN']), 'hasasin': bool(release['AlbumASIN']),
'asin': release['AlbumASIN'], 'asin': release['AlbumASIN'],
@@ -701,15 +757,19 @@ def getHybridRelease(fullreleaselist):
'format': format, 'format': format,
'country': country, 'country': country,
'tracks': release['Tracks'] 'tracks': release['Tracks']
} }
sortable_release_list.append(release_dict) sortable_release_list.append(release_dict)
#necessary to make dates that miss the month and/or day show up after full dates # Necessary to make dates that miss the month and/or day show up after full
# dates
def getSortableReleaseDate(releaseDate): def getSortableReleaseDate(releaseDate):
# Change this value to change the sorting behaviour of none, returning
# 'None' will put it at the top which was normal behaviour for pre-ngs
# versions
if releaseDate == None: if releaseDate == None:
return 'None';#change this value to change the sorting behaviour of none, returning 'None' will put it at the top return 'None';
#which was normal behaviour for pre-ngs versions
if releaseDate.count('-') == 2: if releaseDate.count('-') == 2:
return releaseDate return releaseDate
elif releaseDate.count('-') == 1: elif releaseDate.count('-') == 1:
+14 -11
View File
@@ -21,16 +21,17 @@ from headphones import db, logger, request
from collections import defaultdict from collections import defaultdict
ENTRY_POINT = 'http://ws.audioscrobbler.com/2.0/' TIMEOUT = 60 # seconds
API_KEY = '395e6ec6bb557382fc41fde867bce66f' ENTRY_POINT = "http://ws.audioscrobbler.com/2.0/"
API_KEY = "395e6ec6bb557382fc41fde867bce66f"
def request_lastfm(method, **kwargs): def request_lastfm(method, **kwargs):
""" """
Call a Last.FM API method. Automatically sets the method and API key. Method Call a Last.FM API method. Automatically sets the method and API key. Method
will return the result if no error occured. will return the result if no error occured.
By default, this method will request the JSON format, since it is lighter By default, this method will request the JSON format, since it is more
than XML. lightweight than XML.
""" """
# Prepare request # Prepare request
@@ -40,7 +41,9 @@ def request_lastfm(method, **kwargs):
# Send request # Send request
logger.debug("Calling Last.FM method: %s", method) logger.debug("Calling Last.FM method: %s", method)
data = request.request_json(ENTRY_POINT, timeout=20, params=kwargs) logger.debug("Last.FM call parameters: %s", kwargs)
data = request.request_json(ENTRY_POINT, timeout=TIMEOUT, params=kwargs)
# Parse response and check for errors. # Parse response and check for errors.
if not data: if not data:
@@ -48,20 +51,20 @@ def request_lastfm(method, **kwargs):
return return
if "error" in data: if "error" in data:
logger.debug("Last.FM returned an error: %s", data["message"]) logger.error("Last.FM returned an error: %s", data["message"])
return return
return data return data
def getSimilar(): def getSimilar():
myDB = db.DBConnection() myDB = db.DBConnection()
results = myDB.select('SELECT ArtistID from artists ORDER BY HaveTracks DESC') results = myDB.select("SELECT ArtistID from artists ORDER BY HaveTracks DESC")
logger.info("Fetching similar artists from Last.FM for tag cloud") logger.info("Fetching similar artists from Last.FM for tag cloud")
artistlist = [] artistlist = []
for result in results[:12]: for result in results[:12]:
data = request_lastfm("artist.getsimilar", mbid=result['ArtistId']) data = request_lastfm("artist.getsimilar", mbid=result["ArtistId"])
time.sleep(10) time.sleep(10)
if data and "similarartists" in data: if data and "similarartists" in data:
@@ -94,13 +97,13 @@ def getSimilar():
artist_name, artist_mbid = item[0] artist_name, artist_mbid = item[0]
count = item[1] count = item[1]
myDB.action('INSERT INTO lastfmcloud VALUES( ?, ?, ?)', [artist_name, artist_mbid, count]) myDB.action("INSERT INTO lastfmcloud VALUES( ?, ?, ?)", [artist_name, artist_mbid, count])
logger.debug("Inserted %d artists into Last.FM tag cloud", len(top_list)) logger.debug("Inserted %d artists into Last.FM tag cloud", len(top_list))
def getArtists(): def getArtists():
myDB = db.DBConnection() myDB = db.DBConnection()
results = myDB.select('SELECT ArtistID from artists') results = myDB.select("SELECT ArtistID from artists")
if not headphones.LASTFM_USERNAME: if not headphones.LASTFM_USERNAME:
logger.warn("Last.FM username not set, not importing artists.") logger.warn("Last.FM username not set, not importing artists.")
@@ -129,7 +132,7 @@ def getArtists():
def getTagTopArtists(tag, limit=50): def getTagTopArtists(tag, limit=50):
myDB = db.DBConnection() myDB = db.DBConnection()
results = myDB.select('SELECT ArtistID from artists') results = myDB.select("SELECT ArtistID from artists")
logger.info("Fetching top artists from Last.FM for tag: %s", tag) logger.info("Fetching top artists from Last.FM for tag: %s", tag)
data = request_lastfm("tag.gettopartists", limit=limit, tag=tag) data = request_lastfm("tag.gettopartists", limit=limit, tag=tag)
+70 -57
View File
@@ -15,10 +15,10 @@
import os import os
import glob import glob
from beets.mediafile import MediaFile
import headphones import headphones
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
from headphones import db, logger, helpers, importer, lastfm from headphones import db, logger, helpers, importer, lastfm
# You can scan a single directory and append it to the current library by specifying append=True, ArtistID & ArtistName # You can scan a single directory and append it to the current library by specifying append=True, ArtistID & ArtistName
@@ -27,18 +27,18 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
if cron and not headphones.LIBRARYSCAN: if cron and not headphones.LIBRARYSCAN:
return return
if not dir: if not dir:
if not headphones.MUSIC_DIR: if not headphones.MUSIC_DIR:
return return
else: else:
dir = headphones.MUSIC_DIR dir = headphones.MUSIC_DIR
# If we're appending a dir, it's coming from the post processor which is # If we're appending a dir, it's coming from the post processor which is
# already bytestring # already bytestring
if not append: if not append:
dir = dir.encode(headphones.SYS_ENCODING) dir = dir.encode(headphones.SYS_ENCODING)
if not os.path.isdir(dir): if not os.path.isdir(dir):
logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING, 'replace')) logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING, 'replace'))
return return
@@ -47,21 +47,21 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
new_artists = [] new_artists = []
logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
if not append: if not append:
# Clean up bad filepaths # Clean up bad filepaths
tracks = myDB.select('SELECT Location, TrackID from alltracks WHERE Location IS NOT NULL') tracks = myDB.select('SELECT Location from alltracks WHERE Location IS NOT NULL UNION SELECT Location from tracks WHERE Location IS NOT NULL')
for track in tracks: for track in tracks:
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING) encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING)
if not os.path.isfile(encoded_track_string): if not os.path.isfile(encoded_track_string):
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']]) myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']])
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']]) myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']])
del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName from have') del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName from have')
for track in del_have_tracks: for track in del_have_tracks:
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING) encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace')
if not os.path.isfile(encoded_track_string): if not os.path.isfile(encoded_track_string):
if track['ArtistName']: if track['ArtistName']:
#Make sure deleted files get accounted for when updating artist track counts #Make sure deleted files get accounted for when updating artist track counts
@@ -71,21 +71,21 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
###############myDB.action('DELETE from have') ###############myDB.action('DELETE from have')
bitrates = [] bitrates = []
song_list = [] song_list = []
new_song_count = 0 new_song_count = 0
file_count = 0 file_count = 0
latest_subdirectory = [] latest_subdirectory = []
for r,d,f in os.walk(dir): for r,d,f in os.walk(dir):
#need to abuse slicing to get a copy of the list, doing it directly will skip the element after a deleted one #need to abuse slicing to get a copy of the list, doing it directly will skip the element after a deleted one
#using a list comprehension will not work correctly for nested subdirectories (os.walk keeps its original list) #using a list comprehension will not work correctly for nested subdirectories (os.walk keeps its original list)
for directory in d[:]: for directory in d[:]:
if directory.startswith("."): if directory.startswith("."):
d.remove(directory) d.remove(directory)
for files in f:
for files in f:
# MEDIA_FORMATS = music file extensions, e.g. mp3, flac, etc # MEDIA_FORMATS = music file extensions, e.g. mp3, flac, etc
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
@@ -104,15 +104,14 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
# Try to read the metadata # Try to read the metadata
try: try:
f = MediaFile(song) f = MediaFile(song)
except (FileTypeError, UnreadableFileError):
except: logger.error("Cannot read file media file '%s'. It may be corrupted or not a media file.", unicode_song_path)
logger.error('Cannot read file: ' + unicode_song_path)
continue continue
# Grab the bitrates for the auto detect bit rate option # Grab the bitrates for the auto detect bit rate option
if f.bitrate: if f.bitrate:
bitrates.append(f.bitrate) bitrates.append(f.bitrate)
# Use the album artist over the artist if available # Use the album artist over the artist if available
if f.albumartist: if f.albumartist:
f_artist = f.albumartist f_artist = f.albumartist
@@ -120,8 +119,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
f_artist = f.artist f_artist = f.artist
else: else:
f_artist = None f_artist = None
# Add the song to our song list - # Add the song to our song list -
# TODO: skip adding songs without the minimum requisite information (just a matter of putting together the right if statements) # TODO: skip adding songs without the minimum requisite information (just a matter of putting together the right if statements)
if f_artist and f.album and f.title: if f_artist and f.album and f.title:
@@ -144,7 +143,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
'Format' : f.format, 'Format' : f.format,
'CleanName' : CleanName 'CleanName' : CleanName
} }
#song_list.append(song_dict) #song_list.append(song_dict)
check_exist_song = myDB.action("SELECT * FROM have WHERE Location=?", [unicode_song_path]).fetchone() check_exist_song = myDB.action("SELECT * FROM have WHERE Location=?", [unicode_song_path]).fetchone()
#Only attempt to match songs that are new, haven't yet been matched, or metadata has changed. #Only attempt to match songs that are new, haven't yet been matched, or metadata has changed.
@@ -182,17 +181,17 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"]) song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"])
total_number_of_songs = myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"]).fetchone()[0] total_number_of_songs = myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"]).fetchone()[0]
logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....") logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....")
# Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid) # Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid)
# When we insert into the database, the tracks with the most specific information will overwrite the more general matches # When we insert into the database, the tracks with the most specific information will overwrite the more general matches
##############song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID']) ##############song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID'])
song_list = helpers.multikeysort(song_list, ['ArtistName', 'AlbumTitle']) song_list = helpers.multikeysort(song_list, ['ArtistName', 'AlbumTitle'])
# We'll use this to give a % completion, just because the track matching might take a while # We'll use this to give a % completion, just because the track matching might take a while
song_count = 0 song_count = 0
latest_artist = [] latest_artist = []
for song in song_list: for song in song_list:
latest_artist.append(song['ArtistName']) latest_artist.append(song['ArtistName'])
@@ -200,26 +199,27 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
logger.info("Now matching songs by %s" % song['ArtistName']) logger.info("Now matching songs by %s" % song['ArtistName'])
elif latest_artist[song_count] != latest_artist[song_count-1] and song_count !=0: elif latest_artist[song_count] != latest_artist[song_count-1] and song_count !=0:
logger.info("Now matching songs by %s" % song['ArtistName']) logger.info("Now matching songs by %s" % song['ArtistName'])
#print song['ArtistName']+' - '+song['AlbumTitle']+' - '+song['TrackTitle'] #print song['ArtistName']+' - '+song['AlbumTitle']+' - '+song['TrackTitle']
song_count += 1 song_count += 1
completion_percentage = float(song_count)/total_number_of_songs * 100 completion_percentage = float(song_count)/total_number_of_songs * 100
if completion_percentage%10 == 0: if completion_percentage%10 == 0:
logger.info("Track matching is " + str(completion_percentage) + "% complete") logger.info("Track matching is " + str(completion_percentage) + "% complete")
#THE "MORE-SPECIFIC" CLAUSES HERE HAVE ALL BEEN REMOVED. WHEN RUNNING A LIBRARY SCAN, THE ONLY CLAUSES THAT #THE "MORE-SPECIFIC" CLAUSES HERE HAVE ALL BEEN REMOVED. WHEN RUNNING A LIBRARY SCAN, THE ONLY CLAUSES THAT
#EVER GOT HIT WERE [ARTIST/ALBUM/TRACK] OR CLEANNAME. ARTISTID & RELEASEID ARE NEVER PASSED TO THIS FUNCTION, #EVER GOT HIT WERE [ARTIST/ALBUM/TRACK] OR CLEANNAME. ARTISTID & RELEASEID ARE NEVER PASSED TO THIS FUNCTION,
#ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM #ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM
#(IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT. #(IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT.
if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']: if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']:
track = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone() track = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
have_updated = False
if track: if track:
controlValueDict = { 'ArtistName' : track['ArtistName'], controlValueDict = { 'ArtistName' : track['ArtistName'],
'AlbumTitle' : track['AlbumTitle'], 'AlbumTitle' : track['AlbumTitle'],
'TrackTitle' : track['TrackTitle'] } 'TrackTitle' : track['TrackTitle'] }
newValueDict = { 'Location' : song['Location'], newValueDict = { 'Location' : song['Location'],
'BitRate' : song['BitRate'], 'BitRate' : song['BitRate'],
'Format' : song['Format'] } 'Format' : song['Format'] }
@@ -228,25 +228,27 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
controlValueDict2 = { 'Location' : song['Location']} controlValueDict2 = { 'Location' : song['Location']}
newValueDict2 = { 'Matched' : track['AlbumID']} newValueDict2 = { 'Matched' : track['AlbumID']}
myDB.upsert("have", newValueDict2, controlValueDict2) myDB.upsert("have", newValueDict2, controlValueDict2)
have_updated = True
else: else:
track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone() track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone()
if track: if track:
controlValueDict = { 'CleanName' : track['CleanName']} controlValueDict = { 'CleanName' : track['CleanName']}
newValueDict = { 'Location' : song['Location'], newValueDict = { 'Location' : song['Location'],
'BitRate' : song['BitRate'], 'BitRate' : song['BitRate'],
'Format' : song['Format'] } 'Format' : song['Format'] }
myDB.upsert("tracks", newValueDict, controlValueDict) myDB.upsert("tracks", newValueDict, controlValueDict)
controlValueDict2 = { 'Location' : song['Location']} controlValueDict2 = { 'Location' : song['Location']}
newValueDict2 = { 'Matched' : track['AlbumID']} newValueDict2 = { 'Matched' : track['AlbumID']}
myDB.upsert("have", newValueDict2, controlValueDict2) myDB.upsert("have", newValueDict2, controlValueDict2)
have_updated = True
else: else:
controlValueDict2 = { 'Location' : song['Location']} controlValueDict2 = { 'Location' : song['Location']}
newValueDict2 = { 'Matched' : "Failed"} newValueDict2 = { 'Matched' : "Failed"}
myDB.upsert("have", newValueDict2, controlValueDict2) myDB.upsert("have", newValueDict2, controlValueDict2)
have_updated = True
alltrack = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone() alltrack = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
if alltrack: if alltrack:
controlValueDict = { 'ArtistName' : alltrack['ArtistName'], controlValueDict = { 'ArtistName' : alltrack['ArtistName'],
'AlbumTitle' : alltrack['AlbumTitle'], 'AlbumTitle' : alltrack['AlbumTitle'],
@@ -262,52 +264,55 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
else: else:
alltrack = myDB.action('SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone() alltrack = myDB.action('SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone()
if alltrack: if alltrack:
controlValueDict = { 'CleanName' : alltrack['CleanName']} controlValueDict = { 'CleanName' : alltrack['CleanName']}
newValueDict = { 'Location' : song['Location'], newValueDict = { 'Location' : song['Location'],
'BitRate' : song['BitRate'], 'BitRate' : song['BitRate'],
'Format' : song['Format'] } 'Format' : song['Format'] }
myDB.upsert("alltracks", newValueDict, controlValueDict) myDB.upsert("alltracks", newValueDict, controlValueDict)
controlValueDict2 = { 'Location' : song['Location']} controlValueDict2 = { 'Location' : song['Location']}
newValueDict2 = { 'Matched' : alltrack['AlbumID']} newValueDict2 = { 'Matched' : alltrack['AlbumID']}
myDB.upsert("have", newValueDict2, controlValueDict2) myDB.upsert("have", newValueDict2, controlValueDict2)
else: else:
controlValueDict2 = { 'Location' : song['Location']} # alltracks may not exist if adding album manually, have should only be set to failed if not already updated in tracks
newValueDict2 = { 'Matched' : "Failed"} if not have_updated:
myDB.upsert("have", newValueDict2, controlValueDict2) controlValueDict2 = { 'Location' : song['Location']}
newValueDict2 = { 'Matched' : "Failed"}
myDB.upsert("have", newValueDict2, controlValueDict2)
else: else:
controlValueDict2 = { 'Location' : song['Location']} controlValueDict2 = { 'Location' : song['Location']}
newValueDict2 = { 'Matched' : "Failed"} newValueDict2 = { 'Matched' : "Failed"}
myDB.upsert("have", newValueDict2, controlValueDict2) myDB.upsert("have", newValueDict2, controlValueDict2)
#######myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']]) #######myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
if not append: if not append:
logger.info('Updating scanned artist track counts') logger.info('Updating scanned artist track counts')
# Clean up the new artist list # Clean up the new artist list
unique_artists = {}.fromkeys(new_artists).keys() unique_artists = {}.fromkeys(new_artists).keys()
current_artists = myDB.select('SELECT ArtistName, ArtistID from artists') current_artists = myDB.select('SELECT ArtistName, ArtistID from artists')
#There was a bug where artists with special characters (-,') would show up in new artists. #There was a bug where artists with special characters (-,') would show up in new artists.
artist_list = [f for f in unique_artists if helpers.cleanName(f).lower() not in [helpers.cleanName(x[0]).lower() for x in current_artists]] artist_list = [f for f in unique_artists if helpers.cleanName(f).lower() not in [helpers.cleanName(x[0]).lower() for x in current_artists]]
artists_checked = [f for f in unique_artists if helpers.cleanName(f).lower() in [helpers.cleanName(x[0]).lower() for x in current_artists]] artists_checked = [f for f in unique_artists if helpers.cleanName(f).lower() in [helpers.cleanName(x[0]).lower() for x in current_artists]]
# Update track counts # Update track counts
for artist in artists_checked: for artist in artists_checked:
# Have tracks are selected from tracks table and not all tracks because of duplicates # Have tracks are selected from tracks table and not all tracks because of duplicates
# We update the track count upon an album switch to compliment this # We update the track count upon an album switch to compliment this
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL', [artist])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist])) havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL', [artist])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist]))
#Note, some people complain about having "artist have tracks" > # of tracks total in artist official releases #Note, some people complain about having "artist have tracks" > # of tracks total in artist official releases
# (can fix by getting rid of second len statement) # (can fix by getting rid of second len statement)
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist]) myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist])
logger.info('Found %i new artists' % len(artist_list)) logger.info('Found %i new artists' % len(artist_list))
if len(artist_list): if len(artist_list):
if headphones.ADD_ARTISTS: if headphones.ADD_ARTISTS:
logger.info('Importing %i new artists' % len(artist_list)) logger.info('Importing %i new artists' % len(artist_list))
@@ -317,23 +322,23 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
#myDB.action('DELETE from newartists') #myDB.action('DELETE from newartists')
for artist in artist_list: for artist in artist_list:
myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist]) myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist])
if headphones.DETECT_BITRATE: if headphones.DETECT_BITRATE:
headphones.PREFERRED_BITRATE = sum(bitrates)/len(bitrates)/1000 headphones.PREFERRED_BITRATE = sum(bitrates)/len(bitrates)/1000
else: else:
# If we're appending a new album to the database, update the artists total track counts # If we're appending a new album to the database, update the artists total track counts
logger.info('Updating artist track counts') logger.info('Updating artist track counts')
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [ArtistID])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [ArtistName])) havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [ArtistID])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [ArtistName]))
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID]) myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
update_album_status()
if not append: if not append:
update_album_status()
lastfm.getSimilar() lastfm.getSimilar()
logger.info('Library scan complete') logger.info('Library scan complete')
#ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED #ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED
def update_album_status(AlbumID=None): def update_album_status(AlbumID=None):
myDB = db.DBConnection() myDB = db.DBConnection()
logger.info('Counting matched tracks to mark albums as skipped/downloaded') logger.info('Counting matched tracks to mark albums as skipped/downloaded')
@@ -355,13 +360,21 @@ def update_album_status(AlbumID=None):
album_completion = 0 album_completion = 0
logger.info('Album %s does not have any tracks in database' % album['AlbumTitle']) logger.info('Album %s does not have any tracks in database' % album['AlbumTitle'])
if album_completion >= headphones.ALBUM_COMPLETION_PCT: if album_completion >= headphones.ALBUM_COMPLETION_PCT and album['Status'] == 'Skipped':
new_album_status = "Downloaded" new_album_status = "Downloaded"
# I don't think we want to change Downloaded->Skipped.....
# I think we can only automatically change Skipped->Downloaded when updating
# There was a bug report where this was causing infinite downloads if the album was
# recent, but matched to less than 80%. It would go Downloaded->Skipped->Wanted->Downloaded->Skipped->Wanted->etc....
#else:
# if album['Status'] == "Skipped" or album['Status'] == "Downloaded":
# new_album_status = "Skipped"
# else:
# new_album_status = album['Status']
else: else:
if album['Status'] == "Skipped" or album['Status'] == "Downloaded": new_album_status = album['Status']
new_album_status = "Skipped"
else:
new_album_status = album['Status']
myDB.upsert("albums", {'Status' : new_album_status}, {'AlbumID' : album['AlbumID']}) myDB.upsert("albums", {'Status' : new_album_status}, {'AlbumID' : album['AlbumID']})
if new_album_status != album['Status']: if new_album_status != album['Status']:
logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status)) logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status))
+69 -21
View File
@@ -13,24 +13,30 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import logging
import traceback
import threading
import headphones
from logging import handlers
from headphones import helpers from headphones import helpers
from logutils.queue import QueueHandler, QueueListener
from logging import handlers
import multiprocessing
import contextlib
import headphones
import threading
import traceback
import logging
import sys
import os
# These settings are for file logging only # These settings are for file logging only
FILENAME = 'headphones.log' FILENAME = "headphones.log"
MAX_SIZE = 1000000 # 1 MB MAX_SIZE = 1000000 # 1 MB
MAX_FILES = 5 MAX_FILES = 5
# Headphones logger # Headphones logger
logger = logging.getLogger('headphones') logger = logging.getLogger("headphones")
# Global queue of multiprocessing logging
queue = multiprocessing.Queue()
class LogListHandler(logging.Handler): class LogListHandler(logging.Handler):
""" """
@@ -43,19 +49,65 @@ class LogListHandler(logging.Handler):
headphones.LOG_LIST.insert(0, (helpers.now(), message, record.levelname, record.threadName)) headphones.LOG_LIST.insert(0, (helpers.now(), message, record.levelname, record.threadName))
def initLogger(verbose=1): @contextlib.contextmanager
def listener():
"""
Wrapper that create a QueueListener, starts it and automatically stops it.
To be used in a with statement in the main process, for multiprocessing.
"""
queue_listener = QueueListener(queue, *logger.handlers)
try:
queue_listener.start()
yield
finally:
queue_listener.stop()
def initMultiprocessing():
"""
Remove all handlers and add QueueHandler on top. This should only be called
inside a multiprocessing worker process, since it changes the logger
completely.
"""
for handler in logger.handlers[:]:
logger.removeHandler(handler)
queue_handler = QueueHandler(queue)
queue_handler.setLevel(logging.DEBUG)
logger.addHandler(queue_handler)
# Change current thread name for log record
threading.current_thread().name = multiprocessing.current_process().name
def initLogger(console=False, verbose=False):
""" """
Setup logging for Headphones. It uses the logger instance with the name Setup logging for Headphones. It uses the logger instance with the name
'headphones'. Three log handlers are added: 'headphones'. Three log handlers are added:
* RotatingFileHandler: for the file headphones.log * RotatingFileHandler: for the file headphones.log
* LogListHandler: for Web UI * LogListHandler: for Web UI
* StreamHandler: for console (if verbose > 0) * StreamHandler: for console (if console)
Console logging is only enabled if console is set to True.
""" """
# Close and remove old handlers. This is required to reinit the loggers
# at runtime
for handler in logger.handlers[:]:
# Just make sure it is cleaned up.
if isinstance(handler, handlers.RotatingFileHandler):
handler.close()
elif isinstance(handler, logging.StreamHandler):
handler.flush()
logger.removeHandler(handler)
# Configure the logger to accept all messages # Configure the logger to accept all messages
logger.propagate = False logger.propagate = False
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG if verbose else logging.INFO)
# Setup file logger # Setup file logger
filename = os.path.join(headphones.LOG_DIR, FILENAME) filename = os.path.join(headphones.LOG_DIR, FILENAME)
@@ -69,20 +121,16 @@ def initLogger(verbose=1):
# Add list logger # Add list logger
loglist_handler = LogListHandler() loglist_handler = LogListHandler()
loglist_handler.setLevel(logging.INFO) loglist_handler.setLevel(logging.DEBUG)
logger.addHandler(loglist_handler) logger.addHandler(loglist_handler)
# Setup console logger # Setup console logger
if verbose: if console:
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S') console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setFormatter(console_formatter) console_handler.setFormatter(console_formatter)
console_handler.setLevel(logging.DEBUG)
if verbose == 1:
console_handler.setLevel(logging.INFO)
elif verbose == 2:
console_handler.setLevel(logging.DEBUG)
logger.addHandler(console_handler) logger.addHandler(console_handler)
+8 -8
View File
@@ -27,26 +27,26 @@ def getLyrics(artist, song):
url = 'http://lyrics.wikia.com/api.php' url = 'http://lyrics.wikia.com/api.php'
data = request.request_minidom(url, params=params) data = request.request_minidom(url, params=params)
if not data: if not data:
return return
url = data.getElementsByTagName("url") url = data.getElementsByTagName("url")
if url: if url:
lyricsurl = url[0].firstChild.nodeValue lyricsurl = url[0].firstChild.nodeValue
else: else:
logger.info('No lyrics found for %s - %s' % (artist, song)) logger.info('No lyrics found for %s - %s' % (artist, song))
return return
lyricspage = request.request_content(lyricsurl) lyricspage = request.request_content(lyricsurl)
if not lyricspage: if not lyricspage:
logger.warn('Error fetching lyrics from: %s' % lyricsurl) logger.warn('Error fetching lyrics from: %s' % lyricsurl)
return return
m = re.compile('''<div class='lyricbox'><div class='rtMatcher'>.*?</div>(.*?)<!--''').search(lyricspage) m = re.compile('''<div class='lyricbox'><div class='rtMatcher'>.*?</div>(.*?)<!--''').search(lyricspage)
if not m: if not m:
m = re.compile('''<div class='lyricbox'><span style="padding:1em"><a href="/Category:Instrumental" title="Instrumental">''').search(lyricspage) m = re.compile('''<div class='lyricbox'><span style="padding:1em"><a href="/Category:Instrumental" title="Instrumental">''').search(lyricspage)
if m: if m:
@@ -54,10 +54,10 @@ def getLyrics(artist, song):
else: else:
logger.warn('Cannot find lyrics on: %s' % lyricsurl) logger.warn('Cannot find lyrics on: %s' % lyricsurl)
return return
lyrics = convert_html_entities(m.group(1)).replace('<br />', '\n') lyrics = convert_html_entities(m.group(1)).replace('<br />', '\n')
lyrics = re.sub('<.*?>', '', lyrics) lyrics = re.sub('<.*?>', '', lyrics)
return lyrics return lyrics
def convert_html_entities(s): def convert_html_entities(s):
+205 -120
View File
@@ -13,28 +13,30 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from __future__ import with_statement
import time
import threading
import headphones
from headphones import logger, db, helpers from headphones import logger, db, helpers
from headphones.helpers import multikeysort, replace_all from headphones.helpers import multikeysort, replace_all
import lib.musicbrainzngs as musicbrainzngs import time
from lib.musicbrainzngs import WebServiceError import threading
import headphones
import musicbrainzngs
try:
from collections import OrderedDict
except ImportError:
# Python 2.6.x fallback, from libs
import OrderedDict
mb_lock = threading.Lock() mb_lock = threading.Lock()
# Quick fix to add mirror switching on the fly. Need to probably return the mbhost & mbport that's # Quick fix to add mirror switching on the fly. Need to probably return the mbhost & mbport that's
# being used, so we can send those values to the log # being used, so we can send those values to the log
def startmb(): def startmb():
mbuser = None mbuser = None
mbpass = None mbpass = None
if headphones.MIRROR == "musicbrainz.org": if headphones.MIRROR == "musicbrainz.org":
mbhost = "musicbrainz.org" mbhost = "musicbrainz.org"
mbport = 80 mbport = 80
@@ -51,7 +53,7 @@ def startmb():
sleepytime = 0 sleepytime = 0
else: else:
return False return False
musicbrainzngs.set_useragent("headphones","0.0","https://github.com/rembo10/headphones") musicbrainzngs.set_useragent("headphones","0.0","https://github.com/rembo10/headphones")
musicbrainzngs.set_hostname(mbhost + ":" + str(mbport)) musicbrainzngs.set_hostname(mbhost + ":" + str(mbport))
if sleepytime == 0: if sleepytime == 0:
@@ -66,29 +68,31 @@ def startmb():
logger.warn("No username or password set for VIP server") logger.warn("No username or password set for VIP server")
else: else:
musicbrainzngs.hpauth(mbuser,mbpass) musicbrainzngs.hpauth(mbuser,mbpass)
logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i', mbhost, mbport, sleepytime) logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i', mbhost, mbport, sleepytime)
return True return True
def findArtist(name, limit=1): def findArtist(name, limit=1):
with mb_lock: with mb_lock:
artistlist = [] artistlist = []
artistResults = None artistResults = None
chars = set('!?*') chars = set('!?*-')
if any((c in chars) for c in name): if any((c in chars) for c in name):
name = '"'+name+'"' name = '"'+name+'"'
criteria = {'artist': name.lower()}
try: try:
artistResults = musicbrainzngs.search_artists(query='artist:'+name,limit=limit)['artist-list'] artistResults = musicbrainzngs.search_artists(limit=limit, **criteria)['artist-list']
except WebServiceError, e: except musicbrainzngs.WebServiceError as e:
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
time.sleep(5) time.sleep(5)
if not artistResults: if not artistResults:
return False return False
for result in artistResults: for result in artistResults:
if 'disambiguation' in result: if 'disambiguation' in result:
uniquename = unicode(result['sort-name'] + " (" + result['disambiguation'] + ")") uniquename = unicode(result['sort-name'] + " (" + result['disambiguation'] + ")")
@@ -96,7 +100,7 @@ def findArtist(name, limit=1):
uniquename = unicode(result['sort-name']) uniquename = unicode(result['sort-name'])
if result['name'] != uniquename and limit == 1: if result['name'] != uniquename and limit == 1:
logger.info('Found an artist with a disambiguation: %s - doing an album based search' % name) logger.info('Found an artist with a disambiguation: %s - doing an album based search' % name)
artistdict = findArtistbyAlbum(name) artistdict = findArtistbyAlbum(name)
if not artistdict: if not artistdict:
logger.info('Cannot determine the best match from an artist/album search. Using top match instead') logger.info('Cannot determine the best match from an artist/album search. Using top match instead')
artistlist.append({ artistlist.append({
@@ -106,10 +110,10 @@ def findArtist(name, limit=1):
'id': unicode(result['id']), 'id': unicode(result['id']),
# 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed # 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
# 'score': int(result['ext:score']) # 'score': int(result['ext:score'])
}) })
else: else:
artistlist.append(artistdict) artistlist.append(artistdict)
else: else:
artistlist.append({ artistlist.append({
'name': unicode(result['sort-name']), 'name': unicode(result['sort-name']),
'uniquename': uniquename, 'uniquename': uniquename,
@@ -118,71 +122,119 @@ def findArtist(name, limit=1):
'score': int(result['ext:score']) 'score': int(result['ext:score'])
}) })
return artistlist return artistlist
def findRelease(name, limit=1):
with mb_lock: def findRelease(name, limit=1, artist=None):
with mb_lock:
releaselist = [] releaselist = []
releaseResults = None releaseResults = None
chars = set('!?') # additional artist search
if not artist and ':' in name:
name, artist = name.rsplit(":",1)
chars = set('!?*-')
if any((c in chars) for c in name): if any((c in chars) for c in name):
name = '"'+name+'"' name = '"'+name+'"'
if artist and any((c in chars) for c in artist):
artist = '"'+artist+'"'
try: try:
releaseResults = musicbrainzngs.search_releases(query=name,limit=limit)['release-list'] releaseResults = musicbrainzngs.search_releases(query=name,limit=limit,artist=artist)['release-list']
except WebServiceError, e: #need to update exceptions except musicbrainzngs.WebServiceError as e: #need to update exceptions
logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e))) logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e)))
time.sleep(5) time.sleep(5)
if not releaseResults: if not releaseResults:
return False return False
for result in releaseResults: for result in releaseResults:
releaselist.append({
title = result['title']
if 'disambiguation' in result:
title += ' (' + result['disambiguation'] + ')'
# Get formats and track counts
format_dict = OrderedDict()
formats = ''
tracks = ''
if 'medium-list' in result:
for medium in result['medium-list']:
if 'format' in medium:
format = medium['format']
if format not in format_dict:
format_dict[format] = 0
format_dict[format] += 1
if 'track-count' in medium:
if tracks:
tracks += ' + '
tracks += str(medium['track-count'])
for format, count in format_dict.items():
if formats:
formats += ' + '
if count > 1:
formats += str(count) + 'x'
formats += format
rg_type = ''
if 'type' in result['release-group']:
rg_type = result['release-group']['type']
if rg_type == 'Album' and 'secondary-type-list' in result['release-group']:
secondary_type = result['release-group']['secondary-type-list'][0]
if secondary_type != rg_type:
rg_type = secondary_type
releaselist.append({
'uniquename': unicode(result['artist-credit'][0]['artist']['name']), 'uniquename': unicode(result['artist-credit'][0]['artist']['name']),
'title': unicode(result['title']), 'title': unicode(title),
'id': unicode(result['artist-credit'][0]['artist']['id']), 'id': unicode(result['artist-credit'][0]['artist']['id']),
'albumid': unicode(result['id']), 'albumid': unicode(result['id']),
'url': unicode("http://musicbrainz.org/artist/" + result['artist-credit'][0]['artist']['id']),#probably needs to be changed 'url': unicode("http://musicbrainz.org/artist/" + result['artist-credit'][0]['artist']['id']),#probably needs to be changed
'albumurl': unicode("http://musicbrainz.org/release/" + result['id']),#probably needs to be changed 'albumurl': unicode("http://musicbrainz.org/release/" + result['id']),#probably needs to be changed
'score': int(result['ext:score']) 'score': int(result['ext:score']),
}) 'date': unicode(result['date']) if 'date' in result else '',
'country': unicode(result['country']) if 'country' in result else '',
'formats': unicode(formats),
'tracks': unicode(tracks),
'rgid': unicode(result['release-group']['id']),
'rgtype': unicode(rg_type)
})
return releaselist return releaselist
def getArtist(artistid, extrasonly=False): def getArtist(artistid, extrasonly=False):
with mb_lock: with mb_lock:
artist_dict = {} artist_dict = {}
artist = None artist = None
try: try:
limit = 200 limit = 200
artist = musicbrainzngs.get_artist_by_id(artistid)['artist'] artist = musicbrainzngs.get_artist_by_id(artistid)['artist']
newRgs = None newRgs = None
artist['release-group-list'] = [] artist['release-group-list'] = []
while newRgs == None or len(newRgs) >= limit: while newRgs == None or len(newRgs) >= limit:
newRgs = musicbrainzngs.browse_release_groups(artistid,release_type="album",offset=len(artist['release-group-list']),limit=limit)['release-group-list'] newRgs = musicbrainzngs.browse_release_groups(artistid,release_type="album",offset=len(artist['release-group-list']),limit=limit)['release-group-list']
artist['release-group-list'] += newRgs artist['release-group-list'] += newRgs
except WebServiceError, e: except musicbrainzngs.WebServiceError as e:
logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e)))
time.sleep(5) time.sleep(5)
except Exception,e: except Exception,e:
pass pass
if not artist: if not artist:
return False return False
#if 'disambiguation' in artist: #if 'disambiguation' in artist:
# uniquename = unicode(artist['sort-name'] + " (" + artist['disambiguation'] + ")") # uniquename = unicode(artist['sort-name'] + " (" + artist['disambiguation'] + ")")
#else: #else:
# uniquename = unicode(artist['sort-name']) # uniquename = unicode(artist['sort-name'])
artist_dict['artist_name'] = unicode(artist['name']) artist_dict['artist_name'] = unicode(artist['name'])
# Not using the following values anywhere yet so we don't need to grab them. # Not using the following values anywhere yet so we don't need to grab them.
# Was causing an exception to be raised if they didn't exist. # Was causing an exception to be raised if they didn't exist.
# #
#artist_dict['artist_sortname'] = unicode(artist['sort-name']) #artist_dict['artist_sortname'] = unicode(artist['sort-name'])
#artist_dict['artist_uniquename'] = uniquename #artist_dict['artist_uniquename'] = uniquename
#artist_dict['artist_type'] = unicode(artist['type']) #artist_dict['artist_type'] = unicode(artist['type'])
@@ -193,22 +245,22 @@ def getArtist(artistid, extrasonly=False):
# if 'begin' in artist['life-span']: # if 'begin' in artist['life-span']:
# artist_dict['artist_begindate'] = unicode(artist['life-span']['begin']) # artist_dict['artist_begindate'] = unicode(artist['life-span']['begin'])
# if 'end' in artist['life-span']: # if 'end' in artist['life-span']:
# artist_dict['artist_enddate'] = unicode(artist['life-span']['end']) # artist_dict['artist_enddate'] = unicode(artist['life-span']['end'])
releasegroups = [] releasegroups = []
if not extrasonly: if not extrasonly:
for rg in artist['release-group-list']: for rg in artist['release-group-list']:
if rg['type'] != 'Album': #only add releases without a secondary type if "secondary-type-list" in rg.keys(): #only add releases without a secondary type
continue continue
releasegroups.append({ releasegroups.append({
'title': unicode(rg['title']), 'title': unicode(rg['title']),
'id': unicode(rg['id']), 'id': unicode(rg['id']),
'url': u"http://musicbrainz.org/release-group/" + rg['id'], 'url': u"http://musicbrainz.org/release-group/" + rg['id'],
'type': unicode(rg['type']) 'type': unicode(rg['type'])
}) })
# See if we need to grab extras. Artist specific extras take precedence over global option # See if we need to grab extras. Artist specific extras take precedence over global option
# Global options are set when adding a new artist # Global options are set when adding a new artist
myDB = db.DBConnection() myDB = db.DBConnection()
@@ -218,110 +270,130 @@ def getArtist(artistid, extrasonly=False):
includeExtras = db_artist['IncludeExtras'] includeExtras = db_artist['IncludeExtras']
except IndexError: except IndexError:
includeExtras = False includeExtras = False
if includeExtras: if includeExtras:
# Need to convert extras string from something like '2,5.6' to ['ep','live','remix'] # Need to convert extras string from something like '2,5.6' to ['ep','live','remix'] (append new extras to end)
extras = db_artist['Extras'] if db_artist['Extras']:
extras_list = ["single", "ep", "compilation", "soundtrack", "live", "remix", "spokenword", "audiobook", "other"] extras = map(int, db_artist['Extras'].split(','))
else:
extras = []
extras_list = ["single", "ep", "compilation", "soundtrack", "live", "remix", "spokenword", "audiobook", "other", "dj-mix", "mixtape/street", "broadcast", "interview", "demo"]
includes = [] includes = []
i = 1 i = 1
for extra in extras_list: for extra in extras_list:
if str(i) in extras: if i in extras:
includes.append(extra) includes.append(extra)
i += 1 i += 1
for include in includes: for include in includes:
artist = None mb_extras_list = []
try: try:
artist = musicbrainzngs.get_artist_by_id(artistid,includes=["releases","release-groups"],release_status=['official'],release_type=include)['artist'] limit = 200
except WebServiceError, e: newRgs = None
while newRgs == None or len(newRgs) >= limit:
newRgs = musicbrainzngs.browse_release_groups(artistid,release_type=include,offset=len(mb_extras_list),limit=limit)['release-group-list']
mb_extras_list += newRgs
except musicbrainzngs.WebServiceError as e:
logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e)))
time.sleep(5) time.sleep(5)
if not artist: for rg in mb_extras_list:
continue
for rg in artist['release-group-list']: rg_type = rg['type']
if rg_type == 'Album' and 'secondary-type-list' in rg:
secondary_type = rg['secondary-type-list'][0]
if secondary_type != rg_type:
rg_type = secondary_type
releasegroups.append({ releasegroups.append({
'title': unicode(rg['title']), 'title': unicode(rg['title']),
'id': unicode(rg['id']), 'id': unicode(rg['id']),
'url': u"http://musicbrainz.org/release-group/" + rg['id'], 'url': u"http://musicbrainz.org/release-group/" + rg['id'],
'type': unicode(rg['type']) 'type': unicode(rg_type)
}) })
artist_dict['releasegroups'] = releasegroups artist_dict['releasegroups'] = releasegroups
return artist_dict return artist_dict
def getReleaseGroup(rgid): def getReleaseGroup(rgid):
""" """
Returns a list of releases in a release group Returns a list of releases in a release group
""" """
with mb_lock: with mb_lock:
releaselist = [] releaselist = []
releaseGroup = None releaseGroup = None
try: try:
releaseGroup = musicbrainzngs.get_release_group_by_id(rgid,["artists","releases","media","discids",])['release-group'] releaseGroup = musicbrainzngs.get_release_group_by_id(rgid,["artists","releases","media","discids",])['release-group']
except WebServiceError, e: except musicbrainzngs.WebServiceError as e:
logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e))) logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e)))
time.sleep(5) time.sleep(5)
if not releaseGroup: if not releaseGroup:
return False return False
else: else:
return releaseGroup['release-list'] return releaseGroup['release-list']
def getRelease(releaseid, include_artist_info=True): def getRelease(releaseid, include_artist_info=True):
""" """
Deep release search to get track info Deep release search to get track info
""" """
with mb_lock: with mb_lock:
release = {} release = {}
results = None results = None
try: try:
if include_artist_info: if include_artist_info:
results = musicbrainzngs.get_release_by_id(releaseid,["artists","release-groups","media","recordings"]).get('release') results = musicbrainzngs.get_release_by_id(releaseid,["artists","release-groups","media","recordings"]).get('release')
else: else:
results = musicbrainzngs.get_release_by_id(releaseid,["media","recordings"]).get('release') results = musicbrainzngs.get_release_by_id(releaseid,["media","recordings"]).get('release')
except WebServiceError, e: except musicbrainzngs.WebServiceError as e:
logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e))) logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e)))
time.sleep(5) time.sleep(5)
if not results: if not results:
return False return False
release['title'] = unicode(results['title']) release['title'] = unicode(results['title'])
release['id'] = unicode(results['id']) release['id'] = unicode(results['id'])
release['asin'] = unicode(results['asin']) if 'asin' in results else None release['asin'] = unicode(results['asin']) if 'asin' in results else None
release['date'] = unicode(results['date']) if 'date' in results else None release['date'] = unicode(results['date']) if 'date' in results else None
try: try:
release['format'] = unicode(results['medium-list'][0]['format']) release['format'] = unicode(results['medium-list'][0]['format'])
except: except:
release['format'] = u'Unknown' release['format'] = u'Unknown'
try: try:
release['country'] = unicode(results['country']) release['country'] = unicode(results['country'])
except: except:
release['country'] = u'Unknown' release['country'] = u'Unknown'
if include_artist_info: if include_artist_info:
if 'release-group' in results: if 'release-group' in results:
release['rgid'] = unicode(results['release-group']['id']) release['rgid'] = unicode(results['release-group']['id'])
release['rg_title'] = unicode(results['release-group']['title']) release['rg_title'] = unicode(results['release-group']['title'])
try: try:
release['rg_type'] = unicode(results['release-group']['type']) release['rg_type'] = unicode(results['release-group']['type'])
if release['rg_type'] == 'Album' and 'secondary-type-list' in results['release-group']:
secondary_type = unicode(results['release-group']['secondary-type-list'][0])
if secondary_type != release['rg_type']:
release['rg_type'] = secondary_type
except KeyError: except KeyError:
release['rg_type'] = u'Unknown' release['rg_type'] = u'Unknown'
else: else:
logger.warn("Release " + releaseid + "had no ReleaseGroup associated") logger.warn("Release " + releaseid + "had no ReleaseGroup associated")
@@ -329,7 +401,7 @@ def getRelease(releaseid, include_artist_info=True):
release['artist_id'] = unicode(results['artist-credit'][0]['artist']['id']) release['artist_id'] = unicode(results['artist-credit'][0]['artist']['id'])
release['tracks'] = getTracksFromRelease(results) release['tracks'] = getTracksFromRelease(results)
return release return release
def get_new_releases(rgid,includeExtras=False,forcefull=False): def get_new_releases(rgid,includeExtras=False,forcefull=False):
@@ -345,12 +417,12 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
break #may want to raise an exception here instead ? break #may want to raise an exception here instead ?
newResults = newResults['release-list'] newResults = newResults['release-list']
results += newResults results += newResults
except WebServiceError, e: except musicbrainzngs.WebServiceError as e:
logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e))) logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e)))
time.sleep(5) time.sleep(5)
return False return False
if not results or len(results) == 0: if not results or len(results) == 0:
return False return False
@@ -382,7 +454,7 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
#all official releases should have the Official status included #all official releases should have the Official status included
if not includeExtras and releasedata.get('status') != 'Official': if not includeExtras and releasedata.get('status') != 'Official':
continue continue
release = {} release = {}
rel_id_check = releasedata['id'] rel_id_check = releasedata['id']
artistid = unicode(releasedata['artist-credit'][0]['artist']['id']) artistid = unicode(releasedata['artist-credit'][0]['artist']['id'])
@@ -395,12 +467,16 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
release['AlbumTitle'] = unicode(releasedata['title']) release['AlbumTitle'] = unicode(releasedata['title'])
release['AlbumID'] = unicode(rgid) release['AlbumID'] = unicode(rgid)
release['AlbumASIN'] = unicode(releasedata['asin']) if 'asin' in releasedata else None release['AlbumASIN'] = unicode(releasedata['asin']) if 'asin' in releasedata else None
release['ReleaseDate'] = unicode(releasedata['date']) if 'date' in releasedata else None release['ReleaseDate'] = unicode(releasedata['date']) if 'date' in releasedata else None
release['ReleaseID'] = releasedata['id'] release['ReleaseID'] = releasedata['id']
if 'release-group' not in releasedata: if 'release-group' not in releasedata:
raise Exception('No release group associated with release id ' + releasedata['id'] + ' album id' + rgid) raise Exception('No release group associated with release id ' + releasedata['id'] + ' album id' + rgid)
release['Type'] = unicode(releasedata['release-group']['type']) release['Type'] = unicode(releasedata['release-group']['type'])
if release['Type'] == 'Album' and 'secondary-type-list' in releasedata['release-group']:
secondary_type = unicode(releasedata['release-group']['secondary-type-list'][0])
if secondary_type != release['Type']:
release['Type'] = secondary_type
#making the assumption that the most important artist will be first in the list #making the assumption that the most important artist will be first in the list
if 'artist-credit' in releasedata: if 'artist-credit' in releasedata:
@@ -409,7 +485,7 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
else: else:
logger.warn('Release ' + releasedata['id'] + ' has no Artists associated.') logger.warn('Release ' + releasedata['id'] + ' has no Artists associated.')
return False return False
release['ReleaseCountry'] = unicode(releasedata['country']) if 'country' in releasedata else u'Unknown' release['ReleaseCountry'] = unicode(releasedata['country']) if 'country' in releasedata else u'Unknown'
#assuming that the list will contain media and that the format will be consistent #assuming that the list will contain media and that the format will be consistent
@@ -428,7 +504,7 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
release['ReleaseFormat'] = unicode(packaged_medium) release['ReleaseFormat'] = unicode(packaged_medium)
except: except:
release['ReleaseFormat'] = u'Unknown' release['ReleaseFormat'] = u'Unknown'
release['Tracks'] = getTracksFromRelease(releasedata) release['Tracks'] = getTracksFromRelease(releasedata)
# What we're doing here now is first updating the allalbums & alltracks table to the most # What we're doing here now is first updating the allalbums & alltracks table to the most
@@ -448,11 +524,11 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
} }
myDB.upsert("allalbums", newValueDict, controlValueDict) myDB.upsert("allalbums", newValueDict, controlValueDict)
for track in release['Tracks']: for track in release['Tracks']:
cleanname = helpers.cleanName(release['ArtistName'] + ' ' + release['AlbumTitle'] + ' ' + track['title']) cleanname = helpers.cleanName(release['ArtistName'] + ' ' + release['AlbumTitle'] + ' ' + track['title'])
controlValueDict = {"TrackID": track['id'], controlValueDict = {"TrackID": track['id'],
"ReleaseID": release['ReleaseID']} "ReleaseID": release['ReleaseID']}
@@ -466,20 +542,20 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
"TrackNumber": track['number'], "TrackNumber": track['number'],
"CleanName": cleanname "CleanName": cleanname
} }
match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?', [cleanname]).fetchone() match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?', [cleanname]).fetchone()
if not match: if not match:
match = myDB.action('SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [release['ArtistName'], release['AlbumTitle'], track['title']]).fetchone() match = myDB.action('SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [release['ArtistName'], release['AlbumTitle'], track['title']]).fetchone()
#if not match: #if not match:
#match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone() #match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
if match: if match:
newValueDict['Location'] = match['Location'] newValueDict['Location'] = match['Location']
newValueDict['BitRate'] = match['BitRate'] newValueDict['BitRate'] = match['BitRate']
newValueDict['Format'] = match['Format'] newValueDict['Format'] = match['Format']
#myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']]) #myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
myDB.action('UPDATE have SET Matched=? WHERE Location=?', (release['AlbumID'], match['Location'])) myDB.action('UPDATE have SET Matched=? WHERE Location=?', (release['AlbumID'], match['Location']))
myDB.upsert("alltracks", newValueDict, controlValueDict) myDB.upsert("alltracks", newValueDict, controlValueDict)
num_new_releases = num_new_releases + 1 num_new_releases = num_new_releases + 1
#print releasedata['title'] #print releasedata['title']
@@ -512,19 +588,19 @@ def getTracksFromRelease(release):
'url': u"http://musicbrainz.org/track/" + track['recording']['id'], 'url': u"http://musicbrainz.org/track/" + track['recording']['id'],
'duration': int(track['length']) if 'length' in track else 0 'duration': int(track['length']) if 'length' in track else 0
}) })
totalTracks += 1 totalTracks += 1
return tracks return tracks
# Used when there is a disambiguation # Used when there is a disambiguation
def findArtistbyAlbum(name): def findArtistbyAlbum(name):
myDB = db.DBConnection() myDB = db.DBConnection()
artist = myDB.action('SELECT AlbumTitle from have WHERE ArtistName=? AND AlbumTitle IS NOT NULL ORDER BY RANDOM()', [name]).fetchone() artist = myDB.action('SELECT AlbumTitle from have WHERE ArtistName=? AND AlbumTitle IS NOT NULL ORDER BY RANDOM()', [name]).fetchone()
if not artist: if not artist:
return False return False
# Probably not neccessary but just want to double check # Probably not neccessary but just want to double check
if not artist['AlbumTitle']: if not artist['AlbumTitle']:
return False return False
@@ -532,20 +608,20 @@ def findArtistbyAlbum(name):
term = '"'+artist['AlbumTitle']+'" AND artist:"'+name+'"' term = '"'+artist['AlbumTitle']+'" AND artist:"'+name+'"'
results = None results = None
try: try:
results = musicbrainzngs.search_release_groups(term).get('release-group-list') results = musicbrainzngs.search_release_groups(term).get('release-group-list')
except WebServiceError, e: except musicbrainzngs.WebServiceError as e:
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
time.sleep(5) time.sleep(5)
if not results: if not results:
return False return False
artist_dict = {} artist_dict = {}
for releaseGroup in results: for releaseGroup in results:
newArtist = releaseGroup['artist-credit'][0]['artist'] newArtist = releaseGroup['artist-credit'][0]['artist']
# Only need the artist ID if we're doing an artist+album lookup # Only need the artist ID if we're doing an artist+album lookup
#if 'disambiguation' in newArtist: #if 'disambiguation' in newArtist:
# uniquename = unicode(newArtist['sort-name'] + " (" + newArtist['disambiguation'] + ")") # uniquename = unicode(newArtist['sort-name'] + " (" + newArtist['disambiguation'] + ")")
@@ -557,21 +633,30 @@ def findArtistbyAlbum(name):
#artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id'] #artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id']
#artist_dict['score'] = int(releaseGroup['ext:score']) #artist_dict['score'] = int(releaseGroup['ext:score'])
return artist_dict return artist_dict
def findAlbumID(artist=None, album=None): def findAlbumID(artist=None, album=None):
results = None results = None
chars = set('!?*-')
try: try:
if album and artist: if album and artist:
term = '"'+album+'" AND artist:"'+artist+'"' if any((c in chars) for c in album):
album = '"'+album+'"'
if any((c in chars) for c in artist):
artist = '"'+artist+'"'
criteria = {'release': album.lower()}
criteria['artist'] = artist.lower()
else: else:
term = album if any((c in chars) for c in album):
results = musicbrainzngs.search_release_groups(term,1).get('release-group-list') album = '"'+album+'"'
except WebServiceError, e: criteria = {'release': album.lower()}
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get('release-group-list')
except musicbrainzngs.WebServiceError as e:
logger.warn('Attempt to query MusicBrainz for %s - %s failed (%s)' % (artist, album, str(e))) logger.warn('Attempt to query MusicBrainz for %s - %s failed (%s)' % (artist, album, str(e)))
time.sleep(5) time.sleep(5)
@@ -579,6 +664,6 @@ def findAlbumID(artist=None, album=None):
return False return False
if len(results) < 1: if len(results) < 1:
return False return False
rgid = unicode(results[0]['id']) rgid = unicode(results[0]['id'])
return rgid return rgid
+77 -34
View File
@@ -53,7 +53,7 @@ def encode(albumPath):
time.sleep(1) time.sleep(1)
os.mkdir(tempDirEncode) os.mkdir(tempDirEncode)
except Exception, e: except Exception as e:
logger.exception("Unable to create temporary directory") logger.exception("Unable to create temporary directory")
return None return None
@@ -96,13 +96,18 @@ def encode(albumPath):
encoder = "C:/Program Files/ffmpeg/bin/ffmpeg.exe" encoder = "C:/Program Files/ffmpeg/bin/ffmpeg.exe"
else: else:
encoder="ffmpeg" encoder="ffmpeg"
elif headphones.ENCODER == 'libav':
if headphones.SYS_PLATFORM == "win32":
encoder = "C:/Program Files/libav/bin/avconv.exe"
else:
encoder="avconv"
i=0 i=0
encoder_failed = False encoder_failed = False
jobs = [] jobs = []
for music in musicFiles: for music in musicFiles:
infoMusic=MediaFile(music) infoMusic = MediaFile(music)
encode = False encode = False
if XLD: if XLD:
@@ -141,29 +146,32 @@ def encode(albumPath):
# Encode music files # Encode music files
if len(jobs) > 0: if len(jobs) > 0:
processes = 1
# Use multicore if enabled
if headphones.ENCODER_MULTICORE: if headphones.ENCODER_MULTICORE:
if headphones.ENCODER_MULTICORE_COUNT == 0: if headphones.ENCODER_MULTICORE_COUNT == 0:
processes = multiprocessing.cpu_count() processes = multiprocessing.cpu_count()
else: else:
processes = headphones.ENCODER_MULTICORE_COUNT processes = headphones.ENCODER_MULTICORE_COUNT
logger.debug("Multi-core encoding enabled, %d processes", processes) logger.debug("Multi-core encoding enabled, spawning %d processes",
else: processes)
processes = 1
# Use multiprocessing only if it's worth the overhead. and if it is # Use multiprocessing only if it's worth the overhead. and if it is
# enabled. If not, then use the old fashioned way. # enabled. If not, then use the old fashioned way.
if processes > 1: if processes > 1:
pool = multiprocessing.Pool(processes=processes) with logger.listener():
results = pool.map_async(command_map, jobs) pool = multiprocessing.Pool(processes=processes)
results = pool.map_async(command_map, jobs)
# No new processes will be created, so close it and wait for all # No new processes will be created, so close it and wait for all
# processes to finish # processes to finish
pool.close() pool.close()
pool.join() pool.join()
# Retrieve the results # Retrieve the results
results = results.get() results = results.get()
else: else:
results = map(command_map, jobs) results = map(command_map, jobs)
@@ -178,7 +186,7 @@ def encode(albumPath):
for dest in musicTempFiles: for dest in musicTempFiles:
if not os.path.exists(dest): if not os.path.exists(dest):
encoder_failed = True encoder_failed = True
logger.error('Encoded file \'%s\' does not exist in the destination temp directory', dest) logger.error("Encoded file '%s' does not exist in the destination temp directory", dest)
# No errors, move from temp to parent # No errors, move from temp to parent
if not encoder_failed and musicTempFiles: if not encoder_failed and musicTempFiles:
@@ -204,7 +212,7 @@ def encode(albumPath):
# Return with error if any encoding errors # Return with error if any encoding errors
if encoder_failed: if encoder_failed:
logger.error('One or more files failed to encode, check debuglog and ensure you have the latest version of %s installed', headphones.ENCODER) logger.error("One or more files failed to encode. Ensure you have the latest version of %s installed.", headphones.ENCODER)
return None return None
time.sleep(1) time.sleep(1)
@@ -220,19 +228,31 @@ def encode(albumPath):
def command_map(args): def command_map(args):
""" """
This method is used for the multiprocessing.map() method as a wrapper. Wrapper for the '[multiprocessing.]map()' method, to unpack the arguments
and wrap exceptions.
""" """
# Initialize multiprocessing logger
if multiprocessing.current_process().name != "MainProcess":
logger.initMultiprocessing()
# Start encoding
try: try:
return command(*args) return command(*args)
except Exception, e: except Exception as e:
logger.exception("Encoder exception, will return failed") logger.exception("Encoder raised an exception.")
return False return False
def command(encoder, musicSource ,musicDest, albumPath): def command(encoder, musicSource, musicDest, albumPath):
cmd=[] """
startMusicTime=time.time() Encode a given music file with a certain encoder. Returns True on success,
or False otherwise.
"""
startMusicTime = time.time()
cmd = []
# XLD
if XLD: if XLD:
xldDestDir = os.path.split(musicDest)[0] xldDestDir = os.path.split(musicDest)[0]
cmd = [encoder] cmd = [encoder]
@@ -242,10 +262,11 @@ def command(encoder, musicSource ,musicDest, albumPath):
cmd.extend(['-o']) cmd.extend(['-o'])
cmd.extend([xldDestDir]) cmd.extend([xldDestDir])
# Lame
elif headphones.ENCODER == 'lame': elif headphones.ENCODER == 'lame':
cmd = [encoder] cmd = [encoder]
opts = [] opts = []
if headphones.ADVANCEDENCODER =='': if not headphones.ADVANCEDENCODER:
opts.extend(['-h']) opts.extend(['-h'])
if headphones.ENCODERVBRCBR=='cbr': if headphones.ENCODERVBRCBR=='cbr':
opts.extend(['--resample', str(headphones.SAMPLINGFREQUENCY), '-b', str(headphones.BITRATE)]) opts.extend(['--resample', str(headphones.SAMPLINGFREQUENCY), '-b', str(headphones.BITRATE)])
@@ -259,10 +280,11 @@ def command(encoder, musicSource ,musicDest, albumPath):
opts.extend([musicDest]) opts.extend([musicDest])
cmd.extend(opts) cmd.extend(opts)
# FFmpeg
elif headphones.ENCODER == 'ffmpeg': elif headphones.ENCODER == 'ffmpeg':
cmd = [encoder, '-i', musicSource] cmd = [encoder, '-i', musicSource]
opts = [] opts = []
if headphones.ADVANCEDENCODER =='': if not headphones.ADVANCEDENCODER:
if headphones.ENCODEROUTPUTFORMAT=='ogg': if headphones.ENCODEROUTPUTFORMAT=='ogg':
opts.extend(['-acodec', 'libvorbis']) opts.extend(['-acodec', 'libvorbis'])
if headphones.ENCODEROUTPUTFORMAT=='m4a': if headphones.ENCODEROUTPUTFORMAT=='m4a':
@@ -279,13 +301,30 @@ def command(encoder, musicSource ,musicDest, albumPath):
opts.extend([musicDest]) opts.extend([musicDest])
cmd.extend(opts) cmd.extend(opts)
# Encode # Libav
elif headphones.ENCODER == "libav":
cmd = [encoder, '-i', musicSource]
opts = []
if not headphones.ADVANCEDENCODER:
if headphones.ENCODEROUTPUTFORMAT=='ogg':
opts.extend(['-acodec', 'libvorbis'])
if headphones.ENCODEROUTPUTFORMAT=='m4a':
opts.extend(['-strict', 'experimental'])
if headphones.ENCODERVBRCBR=='cbr':
opts.extend(['-ar', str(headphones.SAMPLINGFREQUENCY), '-ab', str(headphones.BITRATE) + 'k'])
elif headphones.ENCODERVBRCBR=='vbr':
opts.extend(['-aq', str(headphones.ENCODERQUALITY)])
opts.extend(['-y', '-ac', '2', '-vn'])
else:
advanced = (headphones.ADVANCEDENCODER.split())
for tok in advanced:
opts.extend([tok.encode(headphones.SYS_ENCODING)])
opts.extend([musicDest])
cmd.extend(opts)
logger.info('Encoding %s...' % (musicSource.decode(headphones.SYS_ENCODING, 'replace'))) # Prevent Windows from opening a terminal window
logger.debug(subprocess.list2cmdline(cmd))
# stop windows opening the cmd
startupinfo = None startupinfo = None
if headphones.SYS_PLATFORM == "win32": if headphones.SYS_PLATFORM == "win32":
startupinfo = subprocess.STARTUPINFO() startupinfo = subprocess.STARTUPINFO()
try: try:
@@ -293,12 +332,17 @@ def command(encoder, musicSource ,musicDest, albumPath):
except AttributeError: except AttributeError:
startupinfo.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW startupinfo.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW
p = subprocess.Popen(cmd, startupinfo=startupinfo, stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Encode
logger.info('Encoding %s...' % (musicSource.decode(headphones.SYS_ENCODING, 'replace')))
logger.debug(subprocess.list2cmdline(cmd))
stdout, stderr = p.communicate(headphones.ENCODER) process = subprocess.Popen(cmd, startupinfo=startupinfo,
stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = process.communicate(headphones.ENCODER)
# error if return code not zero # Error if return code not zero
if p.returncode: if process.returncode:
logger.error('Encoding failed for %s' % (musicSource.decode(headphones.SYS_ENCODING, 'replace'))) logger.error('Encoding failed for %s' % (musicSource.decode(headphones.SYS_ENCODING, 'replace')))
out = stdout if stdout else stderr out = stdout if stdout else stderr
out = out.decode(headphones.SYS_ENCODING, 'replace') out = out.decode(headphones.SYS_ENCODING, 'replace')
@@ -319,5 +363,4 @@ def getTimeEncode(start):
seconds -= 3600*hours seconds -= 3600*hours
minutes = seconds / 60 minutes = seconds / 60
seconds -= 60*minutes seconds -= 60*minutes
return "%02d:%02d:%02d" % (hours, minutes, seconds) return "%02d:%02d:%02d" % (hours, minutes, seconds)
+262 -144
View File
@@ -13,32 +13,36 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
from headphones import logger, helpers, common, request
from xml.dom import minidom
from httplib import HTTPSConnection
from urllib import urlencode
from pynma import pynma
import base64 import base64
import cherrypy import cherrypy
import urllib import urllib
import urllib2 import urllib2
import headphones import headphones
import simplejson
import os.path import os.path
import subprocess import subprocess
import gntp.notifier import gntp.notifier
import time
import json
from xml.dom import minidom import oauth2 as oauth
from httplib import HTTPSConnection import pythontwitter as twitter
from urllib import urlencode
import lib.oauth2 as oauth
import lib.pythontwitter as twitter
from headphones import logger, helpers, common, request
from headphones.exceptions import ex
try: try:
from urlparse import parse_qsl from urlparse import parse_qsl
except: except ImportError:
from cgi import parse_qsl from cgi import parse_qsl
class GROWL: class GROWL(object):
"""
Growl notifications, for OS X
"""
def __init__(self): def __init__(self):
self.enabled = headphones.GROWL_ENABLED self.enabled = headphones.GROWL_ENABLED
@@ -88,7 +92,9 @@ class GROWL:
# Send it, including an image # Send it, including an image
image_file = os.path.join(str(headphones.PROG_DIR), 'data/images/headphoneslogo.png') image_file = os.path.join(str(headphones.PROG_DIR), 'data/images/headphoneslogo.png')
image = open(image_file, 'rb').read()
with open(image_file, 'rb') as f:
image = f.read()
try: try:
growl.notify( growl.notify(
@@ -114,15 +120,15 @@ class GROWL:
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message') self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
class PROWL: class PROWL(object):
"""
keys = [] Prowl notifications.
priority = [] """
def __init__(self): def __init__(self):
self.enabled = headphones.PROWL_ENABLED self.enabled = headphones.PROWL_ENABLED
self.keys = headphones.PROWL_KEYS self.keys = headphones.PROWL_KEYS
self.priority = headphones.PROWL_PRIORITY self.priority = headphones.PROWL_PRIORITY
def conf(self, options): def conf(self, options):
return cherrypy.config['config'].get('Prowl', options) return cherrypy.config['config'].get('Prowl', options)
@@ -132,7 +138,7 @@ class PROWL:
return return
http_handler = HTTPSConnection("api.prowlapp.com") http_handler = HTTPSConnection("api.prowlapp.com")
data = {'apikey': headphones.PROWL_KEYS, data = {'apikey': headphones.PROWL_KEYS,
'application': 'Headphones', 'application': 'Headphones',
'event': event, 'event': event,
@@ -149,7 +155,7 @@ class PROWL:
if request_status == 200: if request_status == 200:
logger.info(u"Prowl notifications sent.") logger.info(u"Prowl notifications sent.")
return True return True
elif request_status == 401: elif request_status == 401:
logger.info(u"Prowl auth failed: %s" % response.reason) logger.info(u"Prowl auth failed: %s" % response.reason)
return False return False
else: else:
@@ -161,14 +167,29 @@ class PROWL:
return return
def test(self, keys, priority): def test(self, keys, priority):
self.enabled = True self.enabled = True
self.keys = keys self.keys = keys
self.priority = priority self.priority = priority
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message') self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
class XBMC: class MPC(object):
"""
MPC library update
"""
def __init__(self):
pass
def notify( self ):
subprocess.call( ["mpc", "update"] )
class XBMC(object):
"""
XBMC notifications
"""
def __init__(self): def __init__(self):
@@ -191,9 +212,9 @@ class XBMC:
url = host + '/jsonrpc' url = host + '/jsonrpc'
if self.password: if self.password:
response = request.request_json(req, method="POST", data=simplejson.dumps(data), headers=headers, auth=(self.username, self.password)) response = request.request_json(url, method="post", data=json.dumps(data), headers=headers, auth=(self.username, self.password))
else: else:
response = request.request_json(req, method="POST", data=simplejson.dumps(data), headers=headers) response = request.request_json(url, method="post", data=json.dumps(data), headers=headers)
if response: if response:
return response[0]['result'] return response[0]['result']
@@ -207,10 +228,10 @@ class XBMC:
for host in hosts: for host in hosts:
logger.info('Sending library update command to XBMC @ '+host) logger.info('Sending library update command to XBMC @ '+host)
request = self._sendjson(host, 'AudioLibrary.Scan') request = self._sendjson(host, 'AudioLibrary.Scan')
if not request: if not request:
logger.warn('Error sending update request to XBMC') logger.warn('Error sending update request to XBMC')
def notify(self, artist, album, albumartpath): def notify(self, artist, album, albumartpath):
hosts = [x.strip() for x in self.hosts.split(',')] hosts = [x.strip() for x in self.hosts.split(',')]
@@ -236,21 +257,22 @@ class XBMC:
if not request: if not request:
raise Exception raise Exception
except: except Exception:
logger.warn('Error sending notification request to XBMC') logger.error('Error sending notification request to XBMC')
class LMS:
#Class for updating a Logitech Media Server class LMS(object):
"""
Class for updating a Logitech Media Server
"""
def __init__(self): def __init__(self):
self.hosts = headphones.LMS_HOST self.hosts = headphones.LMS_HOST
def _sendjson(self, host):
data = {'id': 1, 'method': 'slim.request', 'params': ["",["rescan"]]} #Had a lot of trouble with simplejson, but this works.
data = simplejson.JSONEncoder().encode(data)
content = {'Content-Type': 'application/json', 'Content-Length': len(data)} def _sendjson(self, host):
data = {'id': 1, 'method': 'slim.request', 'params': ["",["rescan"]]}
data = json.JSONEncoder().encode(data)
content = {'Content-Type': 'application/json'}
req = urllib2.Request(host+'/jsonrpc.js', data, content) req = urllib2.Request(host+'/jsonrpc.js', data, content)
@@ -260,34 +282,28 @@ class LMS:
logger.warn('Error opening LMS url: %s' % e) logger.warn('Error opening LMS url: %s' % e)
return return
response = simplejson.JSONDecoder().decode(handle.read()) response = json.JSONDecoder().decode(handle.read())
server_result = simplejson.dumps(response)
try: try:
return response[0]['result'] return response['result']
except: except:
logger.warn('LMS returned error: %s' % response[0]['error']) logger.warn('LMS returned error: %s' % response['error'])
return return
def update(self): def update(self):
#Send the ["rescan"] command to an LMS server.
#Note that the command must be prefixed with the 'player' that the command is aimed at,
#But with this being a request for the server to update its library, the player is blank, so ""
hosts = [x.strip() for x in self.hosts.split(',')] hosts = [x.strip() for x in self.hosts.split(',')]
for host in hosts: for host in hosts:
logger.info('Sending library rescan command to LMS @ '+host) logger.info('Sending library rescan command to LMS @ '+host)
request = self._sendjson(host) request = self._sendjson(host)
if not request: if not request:
logger.warn('Error sending rescan request to LMS') logger.warn('Error sending rescan request to LMS')
class Plex: class Plex(object):
def __init__(self): def __init__(self):
self.server_hosts = headphones.PLEX_SERVER_HOST self.server_hosts = headphones.PLEX_SERVER_HOST
self.client_hosts = headphones.PLEX_CLIENT_HOST self.client_hosts = headphones.PLEX_CLIENT_HOST
self.username = headphones.PLEX_USERNAME self.username = headphones.PLEX_USERNAME
@@ -297,31 +313,31 @@ class Plex:
username = self.username username = self.username
password = self.password password = self.password
url_command = urllib.urlencode(command) url_command = urllib.urlencode(command)
url = host + '/xbmcCmds/xbmcHttp/?' + url_command url = host + '/xbmcCmds/xbmcHttp/?' + url_command
req = urllib2.Request(url) req = urllib2.Request(url)
if password: if password:
base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '') base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '')
req.add_header("Authorization", "Basic %s" % base64string) req.add_header("Authorization", "Basic %s" % base64string)
logger.info('Plex url: %s' % url) logger.info('Plex url: %s' % url)
try: try:
handle = urllib2.urlopen(req) handle = urllib2.urlopen(req)
except Exception, e: except Exception, e:
logger.warn('Error opening Plex url: %s' % e) logger.warn('Error opening Plex url: %s' % e)
return return
response = handle.read().decode(headphones.SYS_ENCODING) response = handle.read().decode(headphones.SYS_ENCODING)
return response return response
def update(self): def update(self):
# From what I read you can't update the music library on a per directory or per path basis # From what I read you can't update the music library on a per directory or per path basis
# so need to update the whole thing # so need to update the whole thing
@@ -349,7 +365,7 @@ class Plex:
except Exception, e: except Exception, e:
logger.warn("Error updating library section for Plex Media Server: %s" % e) logger.warn("Error updating library section for Plex Media Server: %s" % e)
return False return False
def notify(self, artist, album, albumartpath): def notify(self, artist, album, albumartpath):
hosts = [x.strip() for x in self.client_hosts.split(',')] hosts = [x.strip() for x in self.client_hosts.split(',')]
@@ -361,47 +377,53 @@ class Plex:
for host in hosts: for host in hosts:
logger.info('Sending notification command to Plex Media Server @ '+host) logger.info('Sending notification command to Plex Media Server @ '+host)
try: try:
notification = header + "," + message + "," + time + "," + albumartpath notification = header + "," + message + "," + time + "," + albumartpath
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification('+notification+')'} notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification('+notification+')'}
request = self._sendhttp(host, notifycommand) request = self._sendhttp(host, notifycommand)
if not request: if not request:
raise Exception raise Exception
except: except:
logger.warn('Error sending notification request to Plex Media Server') logger.warn('Error sending notification request to Plex Media Server')
class NMA: class NMA(object):
def notify(self, artist=None, album=None, snatched=None):
title = 'Headphones'
api = headphones.NMA_APIKEY
nma_priority = headphones.NMA_PRIORITY
def __init__(self): logger.debug(u"NMA title: " + title)
logger.debug(u"NMA API: " + api)
self.apikey = headphones.NMA_APIKEY logger.debug(u"NMA Priority: " + str(nma_priority))
self.priority = headphones.NMA_PRIORITY
if snatched:
def _send(self, data): event = snatched + " snatched!"
return request.request_content('https://www.notifymyandroid.com/publicapi/notify', data=data) message = "Headphones has snatched: " + snatched
def notify(self, artist=None, album=None, snatched_nzb=None):
apikey = self.apikey
priority = self.priority
if snatched_nzb:
event = snatched_nzb + " snatched!"
description = "Headphones has snatched: " + snatched_nzb + " and has sent it to SABnzbd+"
else: else:
event = artist + ' - ' + album + ' complete!' event = artist + ' - ' + album + ' complete!'
description = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']' message = "Headphones has downloaded and postprocessed: " + artist + ' [' + album + ']'
data = { 'apikey': apikey, 'application':'Headphones', 'event': event, 'description': description, 'priority': priority}
logger.info('Sending notification request to NotifyMyAndroid') logger.debug(u"NMA event: " + event)
request = self._send(data) logger.debug(u"NMA message: " + message)
if not request:
logger.warn('Error sending notification request to NotifyMyAndroid')
class PUSHBULLET: batch = False
p = pynma.PyNMA()
keys = api.split(',')
p.addkey(keys)
if len(keys) > 1: batch = True
response = p.push(title, event, message, priority=nma_priority, batch_mode=batch)
if not response[api][u'code'] == u'200':
logger.error(u'Could not send notification to NotifyMyAndroid')
return False
else:
return True
class PUSHBULLET(object):
def __init__(self): def __init__(self):
self.apikey = headphones.PUSHBULLET_APIKEY self.apikey = headphones.PUSHBULLET_APIKEY
@@ -415,7 +437,7 @@ class PUSHBULLET:
return return
http_handler = HTTPSConnection("api.pushbullet.com") http_handler = HTTPSConnection("api.pushbullet.com")
data = {'device_iden': headphones.PUSHBULLET_DEVICEID, data = {'device_iden': headphones.PUSHBULLET_DEVICEID,
'type': "note", 'type': "note",
'title': "Headphones", 'title': "Headphones",
@@ -435,7 +457,7 @@ class PUSHBULLET:
if request_status == 200: if request_status == 200:
logger.info(u"PushBullet notifications sent.") logger.info(u"PushBullet notifications sent.")
return True return True
elif request_status >= 400 and request_status < 500: elif request_status >= 400 and request_status < 500:
logger.info(u"PushBullet request failed: %s" % response.reason) logger.info(u"PushBullet request failed: %s" % response.reason)
return False return False
else: else:
@@ -454,22 +476,21 @@ class PUSHBULLET:
self.notify('Main Screen Activate', 'Test Message') self.notify('Main Screen Activate', 'Test Message')
class PUSHALOT(object):
class PUSHALOT:
def notify(self, message, event): def notify(self, message, event):
if not headphones.PUSHALOT_ENABLED: if not headphones.PUSHALOT_ENABLED:
return return
pushalot_authorizationtoken = headphones.PUSHALOT_APIKEY pushalot_authorizationtoken = headphones.PUSHALOT_APIKEY
logger.debug(u"Pushalot event: " + event) logger.debug(u"Pushalot event: " + event)
logger.debug(u"Pushalot message: " + message) logger.debug(u"Pushalot message: " + message)
logger.debug(u"Pushalot api: " + pushalot_authorizationtoken) logger.debug(u"Pushalot api: " + pushalot_authorizationtoken)
http_handler = HTTPSConnection("pushalot.com") http_handler = HTTPSConnection("pushalot.com")
data = {'AuthorizationToken': pushalot_authorizationtoken, data = {'AuthorizationToken': pushalot_authorizationtoken,
'Title': event.encode('utf-8'), 'Title': event.encode('utf-8'),
'Body': message.encode("utf-8") } 'Body': message.encode("utf-8") }
@@ -487,14 +508,14 @@ class PUSHALOT:
if request_status == 200: if request_status == 200:
logger.info(u"Pushalot notifications sent.") logger.info(u"Pushalot notifications sent.")
return True return True
elif request_status == 410: elif request_status == 410:
logger.info(u"Pushalot auth failed: %s" % response.reason) logger.info(u"Pushalot auth failed: %s" % response.reason)
return False return False
else: else:
logger.info(u"Pushalot notification failed.") logger.info(u"Pushalot notification failed.")
return False return False
class Synoindex: class Synoindex(object):
def __init__(self, util_loc='/usr/syno/bin/synoindex'): def __init__(self, util_loc='/usr/syno/bin/synoindex'):
self.util_loc = util_loc self.util_loc = util_loc
@@ -529,19 +550,18 @@ class Synoindex:
if isinstance(path_list, list): if isinstance(path_list, list):
for path in path_list: for path in path_list:
self.notify(path) self.notify(path)
class PUSHOVER:
application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz" class PUSHOVER(object):
keys = []
priority = []
def __init__(self): def __init__(self):
self.enabled = headphones.PUSHOVER_ENABLED self.enabled = headphones.PUSHOVER_ENABLED
self.keys = headphones.PUSHOVER_KEYS self.keys = headphones.PUSHOVER_KEYS
self.priority = headphones.PUSHOVER_PRIORITY self.priority = headphones.PUSHOVER_PRIORITY
if headphones.PUSHOVER_APITOKEN: if headphones.PUSHOVER_APITOKEN:
self.application_token = headphones.PUSHOVER_APITOKEN self.application_token = headphones.PUSHOVER_APITOKEN
pass else:
self.application_token = "LdPCoy0dqC21ktsbEyAVCcwvQiVlsz"
def conf(self, options): def conf(self, options):
return cherrypy.config['config'].get('Pushover', options) return cherrypy.config['config'].get('Pushover', options)
@@ -551,8 +571,8 @@ class PUSHOVER:
return return
http_handler = HTTPSConnection("api.pushover.net") http_handler = HTTPSConnection("api.pushover.net")
data = {'token': self.application_token, data = {'token': self.application_token,
'user': headphones.PUSHOVER_KEYS, 'user': headphones.PUSHOVER_KEYS,
'title': event, 'title': event,
'message': message.encode("utf-8"), 'message': message.encode("utf-8"),
@@ -571,7 +591,7 @@ class PUSHOVER:
if request_status == 200: if request_status == 200:
logger.info(u"Pushover notifications sent.") logger.info(u"Pushover notifications sent.")
return True return True
elif request_status >= 400 and request_status < 500: elif request_status >= 400 and request_status < 500:
logger.info(u"Pushover request failed: %s" % response.reason) logger.info(u"Pushover request failed: %s" % response.reason)
return False return False
else: else:
@@ -583,23 +603,23 @@ class PUSHOVER:
return return
def test(self, keys, priority): def test(self, keys, priority):
self.enabled = True self.enabled = True
self.keys = keys self.keys = keys
self.priority = priority self.priority = priority
self.notify('Main Screen Activate', 'Test Message') self.notify('Main Screen Activate', 'Test Message')
class TwitterNotifier:
consumer_key = "oYKnp2ddX5gbARjqX8ZAAg" class TwitterNotifier(object):
consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk"
REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token'
ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token'
AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize'
SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate'
def __init__(self):
self.consumer_key = "oYKnp2ddX5gbARjqX8ZAAg"
self.consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk"
def notify_snatch(self, title): def notify_snatch(self, title):
if headphones.TWITTER_ONSNATCH: if headphones.TWITTER_ONSNATCH:
self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH]+': '+title+' at '+helpers.now()) self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH]+': '+title+' at '+helpers.now())
@@ -612,37 +632,37 @@ class TwitterNotifier:
return self._notifyTwitter("This is a test notification from Headphones at "+helpers.now(), force=True) return self._notifyTwitter("This is a test notification from Headphones at "+helpers.now(), force=True)
def _get_authorization(self): def _get_authorization(self):
signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
oauth_client = oauth.Client(oauth_consumer) oauth_client = oauth.Client(oauth_consumer)
logger.info('Requesting temp token from Twitter') logger.info('Requesting temp token from Twitter')
resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET') resp, content = oauth_client.request(self.REQUEST_TOKEN_URL, 'GET')
if resp['status'] != '200': if resp['status'] != '200':
logger.info('Invalid respond from Twitter requesting temp token: %s' % resp['status']) logger.info('Invalid respond from Twitter requesting temp token: %s' % resp['status'])
else: else:
request_token = dict(parse_qsl(content)) request_token = dict(parse_qsl(content))
headphones.TWITTER_USERNAME = request_token['oauth_token'] headphones.TWITTER_USERNAME = request_token['oauth_token']
headphones.TWITTER_PASSWORD = request_token['oauth_token_secret'] headphones.TWITTER_PASSWORD = request_token['oauth_token_secret']
return self.AUTHORIZATION_URL+"?oauth_token="+ request_token['oauth_token'] return self.AUTHORIZATION_URL+"?oauth_token="+ request_token['oauth_token']
def _get_credentials(self, key): def _get_credentials(self, key):
request_token = {} request_token = {}
request_token['oauth_token'] = headphones.TWITTER_USERNAME request_token['oauth_token'] = headphones.TWITTER_USERNAME
request_token['oauth_token_secret'] = headphones.TWITTER_PASSWORD request_token['oauth_token_secret'] = headphones.TWITTER_PASSWORD
request_token['oauth_callback_confirmed'] = 'true' request_token['oauth_callback_confirmed'] = 'true'
token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret'])
token.set_verifier(key) token.set_verifier(key)
logger.info('Generating and signing request for an access token using key '+key) logger.info('Generating and signing request for an access token using key '+key)
signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
logger.info('oauth_consumer: '+str(oauth_consumer)) logger.info('oauth_consumer: '+str(oauth_consumer))
@@ -650,10 +670,10 @@ class TwitterNotifier:
logger.info('oauth_client: '+str(oauth_client)) logger.info('oauth_client: '+str(oauth_client))
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key) resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key)
logger.info('resp, content: '+str(resp)+','+str(content)) logger.info('resp, content: '+str(resp)+','+str(content))
access_token = dict(parse_qsl(content)) access_token = dict(parse_qsl(content))
logger.info('access_token: '+str(access_token)) logger.info('access_token: '+str(access_token))
logger.info('resp[status] = '+str(resp['status'])) logger.info('resp[status] = '+str(resp['status']))
if resp['status'] != '200': if resp['status'] != '200':
logger.info('The request for a token with did not succeed: '+str(resp['status']), logger.ERROR) logger.info('The request for a token with did not succeed: '+str(resp['status']), logger.ERROR)
@@ -664,33 +684,131 @@ class TwitterNotifier:
headphones.TWITTER_USERNAME = access_token['oauth_token'] headphones.TWITTER_USERNAME = access_token['oauth_token']
headphones.TWITTER_PASSWORD = access_token['oauth_token_secret'] headphones.TWITTER_PASSWORD = access_token['oauth_token_secret']
return True return True
def _send_tweet(self, message=None): def _send_tweet(self, message=None):
username=self.consumer_key username=self.consumer_key
password=self.consumer_secret password=self.consumer_secret
access_token_key=headphones.TWITTER_USERNAME access_token_key=headphones.TWITTER_USERNAME
access_token_secret=headphones.TWITTER_PASSWORD access_token_secret=headphones.TWITTER_PASSWORD
logger.info(u"Sending tweet: "+message) logger.info(u"Sending tweet: "+message)
api = twitter.Api(username, password, access_token_key, access_token_secret) api = twitter.Api(username, password, access_token_key, access_token_secret)
try: try:
api.PostUpdate(message) api.PostUpdate(message)
except Exception, e: except Exception, e:
logger.info(u"Error Sending Tweet: %s" % e) logger.info(u"Error Sending Tweet: %s" % e)
return False return False
return True return True
def _notifyTwitter(self, message='', force=False): def _notifyTwitter(self, message='', force=False):
prefix = headphones.TWITTER_PREFIX prefix = headphones.TWITTER_PREFIX
if not headphones.TWITTER_ENABLED and not force: if not headphones.TWITTER_ENABLED and not force:
return False return False
return self._send_tweet(prefix+": "+message) return self._send_tweet(prefix+": "+message)
notifier = TwitterNotifier class OSX_NOTIFY(object):
def __init__(self):
try:
self.objc = __import__("objc")
except:
return False
def swizzle(self, cls, SEL, func):
old_IMP = cls.instanceMethodForSelector_(SEL)
def wrapper(self, *args, **kwargs):
return func(self, old_IMP, *args, **kwargs)
new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector,
signature=old_IMP.signature)
self.objc.classAddMethod(cls, SEL, new_IMP)
def notify(self, title, subtitle=None, text=None, sound=True):
try:
self.swizzle(self.objc.lookUpClass('NSBundle'),
b'bundleIdentifier',
self.swizzled_bundleIdentifier)
NSUserNotification = self.objc.lookUpClass('NSUserNotification')
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter')
NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')
if not NSUserNotification or not NSUserNotificationCenter:
return False
pool = NSAutoreleasePool.alloc().init()
notification = NSUserNotification.alloc().init()
notification.setTitle_(title)
if subtitle:
notification.setSubtitle_(subtitle)
if text:
notification.setInformativeText_(text)
if sound:
notification.setSoundName_("NSUserNotificationDefaultSoundName")
notification.setHasActionButton_(False)
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
notification_center.deliverNotification_(notification)
del pool
return True
except Exception, e:
logger.warn('Error sending OS X Notification: %s' % e)
return False
def swizzled_bundleIdentifier(self, original, swizzled):
return 'ade.headphones.osxnotify'
class BOXCAR(object):
def __init__(self):
self.url = 'https://new.boxcar.io/api/notifications'
def notify(self, title, message, rgid=None):
try:
if rgid:
message += '<br></br><a href="http://musicbrainz.org/release-group/%s">MusicBrainz</a>' % rgid
data = urllib.urlencode({
'user_credentials': headphones.BOXCAR_TOKEN,
'notification[title]': title.encode('utf-8'),
'notification[long_message]': message.encode('utf-8'),
'notification[sound]': "done"
})
req = urllib2.Request(self.url)
handle = urllib2.urlopen(req, data)
handle.close()
return True
except urllib2.URLError as e:
logger.warn('Error sending Boxcar2 Notification: %s' % e)
return False
class SubSonicNotifier(object):
def __init__(self):
self.host = headphones.SUBSONIC_HOST
self.username = headphones.SUBSONIC_USERNAME
self.password = headphones.SUBSONIC_PASSWORD
def notify(self, albumpaths):
# Correct URL
if not self.host.lower().startswith("http"):
self.host = "http://" + self.host
if not self.host.lower().endswith("/"):
self.host = self.host + "/"
# Invoke request
request.request_response(self.host + "musicFolderSettings.view?scanNow",
auth=(self.username, self.password))
+60 -17
View File
@@ -35,12 +35,20 @@ from headphones import logger
def sendNZB(nzb): def sendNZB(nzb):
addToTop = False addToTop = False
nzbgetXMLrpc = "http://%(username)s:%(password)s@%(host)s/xmlrpc" nzbgetXMLrpc = "%(username)s:%(password)s@%(host)s/xmlrpc"
if headphones.NZBGET_HOST == None: if headphones.NZBGET_HOST == None:
logger.error(u"No NZBget host found in configuration. Please configure it.") logger.error(u"No NZBget host found in configuration. Please configure it.")
return False return False
if headphones.NZBGET_HOST.startswith('https://'):
nzbgetXMLrpc = 'https://' + nzbgetXMLrpc
headphones.NZBGET_HOST.replace('https://','',1)
else:
nzbgetXMLrpc = 'http://' + nzbgetXMLrpc
headphones.NZBGET_HOST.replace('http://','',1)
url = nzbgetXMLrpc % {"host": headphones.NZBGET_HOST, "username": headphones.NZBGET_USERNAME, "password": headphones.NZBGET_PASSWORD} url = nzbgetXMLrpc % {"host": headphones.NZBGET_HOST, "username": headphones.NZBGET_USERNAME, "password": headphones.NZBGET_PASSWORD}
nzbGetRPC = xmlrpclib.ServerProxy(url) nzbGetRPC = xmlrpclib.ServerProxy(url)
@@ -61,25 +69,60 @@ def sendNZB(nzb):
logger.error(u"Protocol Error: " + e.errmsg) logger.error(u"Protocol Error: " + e.errmsg)
return False return False
# if it's a normal result need to download the NZB content nzbcontent64 = None
if nzb.resultType == "nzb": if nzb.resultType == "nzbdata":
genProvider = GenericProvider("")
data = genProvider.getURL(nzb.url)
if (data == None):
return False
# if we get a raw data result thats even better
elif nzb.resultType == "nzbdata":
data = nzb.extraInfo[0] data = nzb.extraInfo[0]
nzbcontent64 = standard_b64encode(data)
nzbcontent64 = standard_b64encode(data)
logger.info(u"Sending NZB to NZBget") logger.info(u"Sending NZB to NZBget")
logger.debug(u"URL: " + url) logger.debug(u"URL: " + url)
if nzbGetRPC.append(nzb.name + ".nzb", headphones.NZBGET_CATEGORY, addToTop, nzbcontent64): dupekey = ""
logger.debug(u"NZB sent to NZBget successfully") dupescore = 0
return True
else: try:
logger.error(u"NZBget could not add %s to the queue" % (nzb.name + ".nzb")) # Find out if nzbget supports priority (Version 9.0+), old versions beginning with a 0.x will use the old command
nzbget_version_str = nzbGetRPC.version()
nzbget_version = int(nzbget_version_str[:nzbget_version_str.find(".")])
if nzbget_version == 0:
if nzbcontent64 is not None:
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb", headphones.NZBGET_CATEGORY, addToTop, nzbcontent64)
else:
if nzb.resultType == "nzb":
genProvider = GenericProvider("")
data = genProvider.getURL(nzb.url)
if (data == None):
return False
nzbcontent64 = standard_b64encode(data)
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb", headphones.NZBGET_CATEGORY, addToTop, nzbcontent64)
elif nzbget_version == 12:
if nzbcontent64 is not None:
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb", headphones.NZBGET_CATEGORY, headphones.NZBGET_PRIORITY, False,
nzbcontent64, False, dupekey, dupescore, "score")
else:
nzbget_result = nzbGetRPC.appendurl(nzb.name + ".nzb", headphones.NZBGET_CATEGORY, headphones.NZBGET_PRIORITY, False,
nzb.url, False, dupekey, dupescore, "score")
# v13+ has a new combined append method that accepts both (url and content)
# also the return value has changed from boolean to integer
# (Positive number representing NZBID of the queue item. 0 and negative numbers represent error codes.)
elif nzbget_version >= 13:
nzbget_result = True if nzbGetRPC.append(nzb.name + ".nzb", nzbcontent64 if nzbcontent64 is not None else nzb.url,
headphones.NZBGET_CATEGORY, headphones.NZBGET_PRIORITY, False, False, dupekey, dupescore,
"score") > 0 else False
else:
if nzbcontent64 is not None:
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb", headphones.NZBGET_CATEGORY, headphones.NZBGET_PRIORITY, False,
nzbcontent64)
else:
nzbget_result = nzbGetRPC.appendurl(nzb.name + ".nzb", headphones.NZBGET_CATEGORY, headphones.NZBGET_PRIORITY, False,
nzb.url)
if nzbget_result:
logger.debug(u"NZB sent to NZBget successfully")
return True
else:
logger.error(u"NZBget could not add %s to the queue" % (nzb.name + ".nzb"))
return False
except:
logger.error(u"Connect Error to NZBget: could not add %s to the queue" % (nzb.name + ".nzb"))
return False return False
File diff suppressed because it is too large Load Diff
+97 -11
View File
@@ -1,3 +1,18 @@
# This file is part of Headphones.
#
# Headphones 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.
#
# Headphones 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 Headphones. If not, see <http://www.gnu.org/licenses/>.
from headphones import logger from headphones import logger
from xml.dom import minidom from xml.dom import minidom
@@ -5,8 +20,10 @@ from bs4 import BeautifulSoup
import requests import requests
import feedparser import feedparser
import headphones
def request_response(url, method="get", auto_raise=True, whitelist_status_code=None, **kwargs): def request_response(url, method="get", auto_raise=True,
whitelist_status_code=None, **kwargs):
""" """
Convenient wrapper for `requests.get', which will capture the exceptions and Convenient wrapper for `requests.get', which will capture the exceptions and
log them. On success, the Response object is returned. In case of a log them. On success, the Response object is returned. In case of a
@@ -17,9 +34,13 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N
if whitelist_status_code and type(whitelist_status_code) != list: if whitelist_status_code and type(whitelist_status_code) != list:
whitelist_status_code = [whitelist_status_code] whitelist_status_code = [whitelist_status_code]
# Disable verification of SSL certificates if requested. Note: this could
# pose a security issue!
kwargs["verify"] = headphones.VERIFY_SSL_CERT
# Map method to the request.XXX method. This is a simple hack, but it allows # Map method to the request.XXX method. This is a simple hack, but it allows
# requests to apply more magic per method. See lib/requests/api.py. # requests to apply more magic per method. See lib/requests/api.py.
request_method = getattr(requests, method) request_method = getattr(requests, method.lower())
try: try:
# Request the URL # Request the URL
@@ -30,22 +51,41 @@ def request_response(url, method="get", auto_raise=True, whitelist_status_code=N
# is white listed. # is white listed.
if whitelist_status_code and auto_raise: if whitelist_status_code and auto_raise:
if response.status_code not in whitelist_status_code: if response.status_code not in whitelist_status_code:
logger.debug("Response status code %d is not white listed, raising exception", response.status_code) try:
response.raise_for_status() response.raise_for_status()
except:
logger.debug("Response status code %d is not white " \
"listed, raised exception", response.status_code)
raise
elif auto_raise: elif auto_raise:
response.raise_for_status() response.raise_for_status()
return response return response
except requests.ConnectionError: except requests.ConnectionError:
logger.error("Unable to connect to remote host.") logger.error("Unable to connect to remote host. Check if the remote " \
"host is up and running.")
except requests.Timeout: except requests.Timeout:
logger.error("Request timed out.") logger.error("Request timed out. The remote host did not respeond " \
except requests.HTTPError, e: "timely.")
except requests.HTTPError as e:
if e.response is not None: if e.response is not None:
logger.error("Request raise HTTP error with status code: %d", e.response.status_code) if e.response.status_code >= 500:
cause = "remote server error"
elif e.response.status_code >= 400:
cause = "local client error"
else:
# I don't think we will end up here, but for completeness
cause = "unknown"
logger.error("Request raise HTTP error with status code %d (%s).",
e.response.status_code, cause)
# Debug response
if headphones.VERBOSE:
server_message(e.response)
else: else:
logger.error("Request raised HTTP error.") logger.error("Request raised HTTP error.")
except requests.RequestException, e: except requests.RequestException as e:
logger.error("Request raised exception: %s", e) logger.error("Request raised exception: %s", e)
def request_soup(url, **kwargs): def request_soup(url, **kwargs):
@@ -88,12 +128,16 @@ def request_json(url, **kwargs):
result = response.json() result = response.json()
if validator and not validator(result): if validator and not validator(result):
logger.error("JSON validation result vailed") logger.error("JSON validation result failed")
else: else:
return result return result
except ValueError: except ValueError:
logger.error("Response returned invalid JSON data") logger.error("Response returned invalid JSON data")
# Debug response
if headphones.VERBOSE:
server_message(response)
def request_content(url, **kwargs): def request_content(url, **kwargs):
""" """
Wrapper for `request_response', which will return the raw content. Wrapper for `request_response', which will return the raw content.
@@ -112,4 +156,46 @@ def request_feed(url, **kwargs):
response = request_response(url, **kwargs) response = request_response(url, **kwargs)
if response is not None: if response is not None:
return feedparser.parse(response.content) return feedparser.parse(response.content)
def server_message(response):
"""
Extract server message from response and log in to logger with DEBUG level.
Some servers return extra information in the result. Try to parse it for
debugging purpose. Messages are limited to 150 characters, since it may
return the whole page in case of normal web page URLs
"""
message = None
# First attempt is to 'read' the response as HTML
if "text/html" in response.headers.get("content-type"):
try:
soup = BeautifulSoup(response.content, "html5lib")
except Exception:
pass
# Find body and cleanup common tags to grab content, which probably
# contains the message.
message = soup.find("body")
for element in ["header", "script", "footer", "nav", "input",
"textarea"]:
for tag in soup.find_all(element):
tag.replaceWith("")
message = message.text if message else soup.text
message = message.strip()
# Second attempt is to just take the response
if message is None:
message = response.content.strip()
if message:
# Truncate message if it is too long.
if len(message) > 150:
message = message[:150] + "..."
logger.debug("Server responded with message: %s", message)
+162 -191
View File
@@ -1,191 +1,162 @@
# This file is part of Headphones. # This file is part of Headphones.
# #
# Headphones is free software: you can redistribute it and/or modify # Headphones is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# Headphones is distributed in the hope that it will be useful, # Headphones is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>. # along with Headphones. If not, see <http://www.gnu.org/licenses/>.
##################################### #####################################
## Stolen from Sick-Beard's sab.py ## ## Stolen from Sick-Beard's sab.py ##
##################################### #####################################
import urllib, httplib import MultipartPostHandler
import datetime import headphones
import datetime
import headphones import cookielib
import urllib2
from lib import MultipartPostHandler import httplib
import urllib2, cookielib import urllib
import ast import ast
from headphones.common import USER_AGENT from headphones.common import USER_AGENT
from headphones import logger from headphones import logger
from headphones import notifiers, helpers from headphones import notifiers, helpers
def sendNZB(nzb): def sendNZB(nzb):
params = {} params = {}
if headphones.SAB_USERNAME: if headphones.SAB_USERNAME:
params['ma_username'] = headphones.SAB_USERNAME params['ma_username'] = headphones.SAB_USERNAME
if headphones.SAB_PASSWORD: if headphones.SAB_PASSWORD:
params['ma_password'] = headphones.SAB_PASSWORD params['ma_password'] = headphones.SAB_PASSWORD
if headphones.SAB_APIKEY: if headphones.SAB_APIKEY:
params['apikey'] = headphones.SAB_APIKEY params['apikey'] = headphones.SAB_APIKEY
if headphones.SAB_CATEGORY: if headphones.SAB_CATEGORY:
params['cat'] = headphones.SAB_CATEGORY params['cat'] = headphones.SAB_CATEGORY
# if it's a normal result we just pass SAB the URL # if it's a normal result we just pass SAB the URL
if nzb.resultType == "nzb": if nzb.resultType == "nzb":
# for newzbin results send the ID to sab specifically # for newzbin results send the ID to sab specifically
if nzb.provider.getID() == 'newzbin': if nzb.provider.getID() == 'newzbin':
id = nzb.provider.getIDFromURL(nzb.url) id = nzb.provider.getIDFromURL(nzb.url)
if not id: if not id:
logger.info("Unable to send NZB to sab, can't find ID in URL "+str(nzb.url)) logger.info("Unable to send NZB to sab, can't find ID in URL "+str(nzb.url))
return False return False
params['mode'] = 'addid' params['mode'] = 'addid'
params['name'] = id params['name'] = id
else: else:
params['mode'] = 'addurl' params['mode'] = 'addurl'
params['name'] = nzb.url params['name'] = nzb.url
# if we get a raw data result we want to upload it to SAB # if we get a raw data result we want to upload it to SAB
elif nzb.resultType == "nzbdata": elif nzb.resultType == "nzbdata":
# Sanitize the file a bit, since we can only use ascii chars with MultiPartPostHandler # Sanitize the file a bit, since we can only use ascii chars with MultiPartPostHandler
nzbdata = helpers.latinToAscii(nzb.extraInfo[0]) nzbdata = helpers.latinToAscii(nzb.extraInfo[0])
params['mode'] = 'addfile' params['mode'] = 'addfile'
multiPartParams = {"nzbfile": (nzb.name+".nzb", nzbdata)} multiPartParams = {"nzbfile": (helpers.latinToAscii(nzb.name)+".nzb", nzbdata)}
if not headphones.SAB_HOST.startswith('http'): if not headphones.SAB_HOST.startswith('http'):
headphones.SAB_HOST = 'http://' + headphones.SAB_HOST headphones.SAB_HOST = 'http://' + headphones.SAB_HOST
if headphones.SAB_HOST.endswith('/'): if headphones.SAB_HOST.endswith('/'):
headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1] headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1]
url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params) url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
try: try:
if nzb.resultType == "nzb": if nzb.resultType == "nzb":
f = urllib.urlopen(url) f = urllib.urlopen(url)
elif nzb.resultType == "nzbdata": elif nzb.resultType == "nzbdata":
cookies = cookielib.CookieJar() cookies = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies),
MultipartPostHandler.MultipartPostHandler) MultipartPostHandler.MultipartPostHandler)
req = urllib2.Request(url, req = urllib2.Request(url,
multiPartParams, multiPartParams,
headers={'User-Agent': USER_AGENT}) headers={'User-Agent': USER_AGENT})
f = opener.open(req) f = opener.open(req)
except (EOFError, IOError), e: except (EOFError, IOError), e:
logger.error(u"Unable to connect to SAB with URL: %s" % url) logger.error(u"Unable to connect to SAB with URL: %s" % url)
return False return False
except httplib.InvalidURL, e: except httplib.InvalidURL, e:
logger.error(u"Invalid SAB host, check your config. Current host: %s" % headphones.SAB_HOST) logger.error(u"Invalid SAB host, check your config. Current host: %s" % headphones.SAB_HOST)
return False return False
except Exception, e: except Exception, e:
logger.error(u"Error: " + str(e)) logger.error(u"Error: " + str(e))
return False return False
if f == None: if f == None:
logger.info(u"No data returned from SABnzbd, NZB not sent") logger.info(u"No data returned from SABnzbd, NZB not sent")
return False return False
try: try:
result = f.readlines() result = f.readlines()
except Exception, e: except Exception, e:
logger.info(u"Error trying to get result from SAB, NZB not sent: ") logger.info(u"Error trying to get result from SAB, NZB not sent: ")
return False return False
if len(result) == 0: if len(result) == 0:
logger.info(u"No data returned from SABnzbd, NZB not sent") logger.info(u"No data returned from SABnzbd, NZB not sent")
return False return False
sabText = result[0].strip() sabText = result[0].strip()
logger.info(u"Result text from SAB: " + sabText) logger.info(u"Result text from SAB: " + sabText)
if sabText == "ok": if sabText == "ok":
logger.info(u"NZB sent to SAB successfully") logger.info(u"NZB sent to SAB successfully")
if headphones.GROWL_ENABLED and headphones.GROWL_ONSNATCH: return True
logger.info(u"Sending Growl notification") elif sabText == "Missing authentication":
growl = notifiers.GROWL() logger.info(u"Incorrect username/password sent to SAB, NZB not sent")
growl.notify(nzb.name,"Download started") return False
if headphones.PROWL_ENABLED and headphones.PROWL_ONSNATCH: else:
logger.info(u"Sending Prowl notification") logger.info(u"Unknown failure sending NZB to sab. Return text is: " + sabText)
prowl = notifiers.PROWL() return False
prowl.notify(nzb.name,"Download started")
if headphones.PUSHOVER_ENABLED and headphones.PUSHOVER_ONSNATCH: def checkConfig():
logger.info(u"Sending Pushover notification")
prowl = notifiers.PUSHOVER() params = { 'mode' : 'get_config',
prowl.notify(nzb.name,"Download started") 'section' : 'misc'
if headphones.PUSHBULLET_ENABLED and headphones.PUSHBULLET_ONSNATCH: }
logger.info(u"Sending PushBullet notification")
pushbullet = notifiers.PUSHBULLET() if headphones.SAB_USERNAME:
pushbullet.notify(nzb.name + " has been snatched!", "Download started") params['ma_username'] = headphones.SAB_USERNAME
if headphones.TWITTER_ENABLED and headphones.TWITTER_ONSNATCH: if headphones.SAB_PASSWORD:
logger.info(u"Sending Twitter notification") params['ma_password'] = headphones.SAB_PASSWORD
twitter = notifiers.TwitterNotifier() if headphones.SAB_APIKEY:
twitter.notify_snatch(nzb.name) params['apikey'] = headphones.SAB_APIKEY
if headphones.NMA_ENABLED and headphones.NMA_ONSNATCH:
logger.debug(u"Sending NMA notification") if not headphones.SAB_HOST.startswith('http'):
nma = notifiers.NMA() headphones.SAB_HOST = 'http://' + headphones.SAB_HOST
nma.notify(snatched_nzb=nzb.name)
if headphones.PUSHALOT_ENABLED and headphones.PUSHALOT_ONSNATCH: if headphones.SAB_HOST.endswith('/'):
logger.info(u"Sending Pushalot notification") headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1]
pushalot = notifiers.PUSHALOT()
pushalot.notify(nzb.name,"Download started") url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
return True try:
elif sabText == "Missing authentication": f = urllib.urlopen(url).read()
logger.info(u"Incorrect username/password sent to SAB, NZB not sent") except Exception, e:
return False logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)")
else: return (0, 0)
logger.info(u"Unknown failure sending NZB to sab. Return text is: " + sabText)
return False config_options = ast.literal_eval(f)
def checkConfig(): replace_spaces = config_options['misc']['replace_spaces']
replace_dots = config_options['misc']['replace_dots']
params = { 'mode' : 'get_config',
'section' : 'misc' return (replace_spaces, replace_dots)
}
if headphones.SAB_USERNAME:
params['ma_username'] = headphones.SAB_USERNAME
if headphones.SAB_PASSWORD:
params['ma_password'] = headphones.SAB_PASSWORD
if headphones.SAB_APIKEY:
params['apikey'] = headphones.SAB_APIKEY
if not headphones.SAB_HOST.startswith('http'):
headphones.SAB_HOST = 'http://' + headphones.SAB_HOST
if headphones.SAB_HOST.endswith('/'):
headphones.SAB_HOST = headphones.SAB_HOST[0:len(headphones.SAB_HOST)-1]
url = headphones.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
try:
f = urllib.urlopen(url).read()
except Exception, e:
logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)")
return (0, 0)
config_options = ast.literal_eval(f)
replace_spaces = config_options['misc']['replace_spaces']
replace_dots = config_options['misc']['replace_dots']
return (replace_spaces, replace_dots)
+395 -374
View File
File diff suppressed because it is too large Load Diff
+210 -158
View File
@@ -4,23 +4,29 @@
# Headphones rutracker.org search # Headphones rutracker.org search
# Functions called from searcher.py # Functions called from searcher.py
import urllib from headphones import logger, db, utorrent
import urllib2
import cookielib from bencode import bencode as bencode, bdecode
from urlparse import urlparse from urlparse import urlparse
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import headphones
from headphones import logger, db
import lib.bencode as bencode
import os
from tempfile import mkdtemp from tempfile import mkdtemp
from hashlib import sha1
import headphones
import requests
import cookielib
import urllib2
import urllib
import re
import os
class Rutracker(): class Rutracker():
logged_in = False logged_in = False
# Stores a number of login attempts to prevent recursion. # Stores a number of login attempts to prevent recursion.
#login_counter = 0 #login_counter = 0
def __init__(self): def __init__(self):
self.cookiejar = cookielib.CookieJar() self.cookiejar = cookielib.CookieJar()
@@ -29,42 +35,40 @@ class Rutracker():
def login(self, login, password): def login(self, login, password):
"""Implements tracker login procedure.""" """Implements tracker login procedure."""
self.logged_in = False self.logged_in = False
if login is None or password is None: if login is None or password is None:
return False return False
#self.login_counter += 1 #self.login_counter += 1
# No recursion wanted. # No recursion wanted.
#if self.login_counter > 1: #if self.login_counter > 1:
# return False # return False
params = urllib.urlencode({"login_username" : login, params = urllib.urlencode({"login_username" : login,
"login_password" : password, "login_password" : password,
"login" : "Вход"}) "login" : "Вход"})
try: try:
self.opener.open("http://login.rutracker.org/forum/login.php", params) self.opener.open("http://login.rutracker.org/forum/login.php", params)
except : except Exception:
pass pass
# Check if we're logged in # Check if we're logged in
for cookie in self.cookiejar: for cookie in self.cookiejar:
if cookie.name == 'bb_data': if cookie.name == 'bb_data':
self.logged_in = True self.logged_in = True
return self.logged_in return self.logged_in
def searchurl(self, artist, album, year, format): def searchurl(self, artist, album, year, format):
""" """
Return the search url Return the search url
""" """
# Build search url # Build search url
searchterm = '' searchterm = ''
if artist != 'Various Artists': if artist != 'Various Artists':
searchterm = artist searchterm = artist
@@ -72,202 +76,197 @@ class Rutracker():
searchterm = searchterm + album searchterm = searchterm + album
searchterm = searchterm + ' ' searchterm = searchterm + ' '
searchterm = searchterm + year searchterm = searchterm + year
providerurl = "http://rutracker.org/forum/tracker.php" providerurl = "http://rutracker.org/forum/tracker.php"
if format == 'lossless': if format == 'lossless':
format = '+lossless' format = '+lossless'
elif format == 'lossless+mp3': elif format == 'lossless+mp3':
format = '+lossless||mp3||aac' format = '+lossless||mp3||aac'
else: else:
format = '+mp3||aac' format = '+mp3||aac'
# sort by size, descending. # sort by size, descending.
sort = '&o=7&s=2' sort = '&o=7&s=2'
searchurl = "%s?nm=%s%s%s" % (providerurl, urllib.quote(searchterm), format, sort) searchurl = "%s?nm=%s%s%s" % (providerurl, urllib.quote(searchterm), format, sort)
return searchurl return searchurl
def search(self, searchurl, maxsize, minseeders, albumid, bitrate): def search(self, searchurl, maxsize, minseeders, albumid):
""" """
Parse the search results and return valid torrent list Parse the search results and return valid torrent list
""" """
titles = [] titles = []
urls = [] urls = []
seeders = [] seeders = []
sizes = [] sizes = []
torrentlist = [] torrentlist = []
rulist = [] rulist = []
try: try:
page = self.opener.open(searchurl, timeout=60) page = self.opener.open(searchurl, timeout=60)
soup = BeautifulSoup(page.read()) soup = BeautifulSoup(page.read())
# Debug # Debug
#logger.debug (soup.prettify()) #logger.debug (soup.prettify())
# Title # Title
for link in soup.find_all('a', attrs={'class' : 'med tLink hl-tags bold'}):
for link in soup.find_all('a', attrs={'class' : 'med tLink hl-tags bold'}):
title = link.get_text() title = link.get_text()
titles.append(title) titles.append(title)
# Download URL
# Download URL
for link in soup.find_all('a', attrs={'class' : 'small tr-dl dl-stub'}): for link in soup.find_all('a', attrs={'class' : 'small tr-dl dl-stub'}):
url = link.get('href') url = link.get('href')
urls.append(url) urls.append(url)
# Seeders
# Seeders
for link in soup.find_all('b', attrs={'class' : 'seedmed'}): for link in soup.find_all('b', attrs={'class' : 'seedmed'}):
seeder = link.get_text() seeder = link.get_text()
seeders.append(seeder) seeders.append(seeder)
# Size # Size
for link in soup.find_all('td', attrs={'class' : 'row4 small nowrap tor-size'}):
for link in soup.find_all('td', attrs={'class' : 'row4 small nowrap tor-size'}):
size = link.u.string size = link.u.string
sizes.append(size) sizes.append(size)
except : except :
pass pass
# Combine lists # Combine lists
torrentlist = zip(titles, urls, seeders, sizes) torrentlist = zip(titles, urls, seeders, sizes)
# return if nothing found # return if nothing found
if not torrentlist: if not torrentlist:
return False return False
# get headphones track count for album, return if not found
myDB = db.DBConnection()
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
hptrackcount = len(tracks)
if not hptrackcount:
logger.info('headphones track info not found, cannot compare to torrent')
return False
# Return all valid entries, ignored, required words now checked in searcher.py
#unwantedlist = ['promo', 'vinyl', '[lp]', 'songbook', 'tvrip', 'hdtv', 'dvd']
formatlist = ['ape', 'flac', 'ogg', 'm4a', 'aac', 'mp3', 'wav', 'aif'] # don't bother checking track counts anymore, let searcher filter instead
deluxelist = ['deluxe', 'edition', 'japanese', 'exclusive'] # leave code in just in case
check_track_count = False
if check_track_count:
# get headphones track count for album, return if not found
myDB = db.DBConnection()
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
hptrackcount = len(tracks)
if not hptrackcount:
logger.info('headphones track info not found, cannot compare to torrent')
return False
# Return all valid entries, ignored, required words now checked in searcher.py
#unwantedlist = ['promo', 'vinyl', '[lp]', 'songbook', 'tvrip', 'hdtv', 'dvd']
formatlist = ['ape', 'flac', 'ogg', 'm4a', 'aac', 'mp3', 'wav', 'aif']
deluxelist = ['deluxe', 'edition', 'japanese', 'exclusive']
for torrent in torrentlist: for torrent in torrentlist:
returntitle = torrent[0].encode('utf-8') returntitle = torrent[0].encode('utf-8')
url = torrent[1] url = torrent[1]
seeders = torrent[2] seeders = torrent[2]
size = torrent[3] size = torrent[3]
title = returntitle.lower()
if int(size) <= maxsize and int(seeders) >= minseeders:
# Check torrent info
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
self.cookiejar.set_cookie(cookielib.Cookie(version=0, name='bb_dl', value=torrent_id, port=None, port_specified=False, domain='.rutracker.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False))
# Debug
#for cookie in self.cookiejar:
# logger.debug ('Cookie: %s' % cookie)
try:
page = self.opener.open(url)
torrent = page.read()
if torrent:
decoded = bencode.bdecode(torrent)
metainfo = decoded['info']
page.close ()
except Exception, e:
logger.error('Error getting torrent: %s' % e)
return False
# get torrent track count and check for cue
trackcount = 0
cuecount = 0
if 'files' in metainfo: # multi
for pathfile in metainfo['files']:
path = pathfile['path']
for file in path:
if any(file.lower().endswith('.' + x.lower()) for x in formatlist):
trackcount += 1
if '.cue' in file:
cuecount += 1
#Torrent topic page
topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id
logger.debug ('torrent title: %s' % title)
logger.debug ('headphones trackcount: %s' % hptrackcount)
logger.debug ('rutracker trackcount: %s' % trackcount)
# If torrent track count less than headphones track count, and there's a cue, then attempt to get track count from log(s) if int(size) <= maxsize and int(seeders) >= minseeders:
# This is for the case where we have a single .flac/.wav which can be split by cue
# Not great, but shouldn't be doing this too often #Torrent topic page
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
totallogcount = 0 topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id
if trackcount < hptrackcount and cuecount > 0 and cuecount < hptrackcount:
page = self.opener.open(topicurl, timeout=60) # add to list
soup = BeautifulSoup(page.read()) if not check_track_count:
findtoc = soup.find_all(text='TOC of the extracted CD')
if not findtoc:
findtoc = soup.find_all(text='TOC извлечённого CD')
for toc in findtoc:
logcount = 0
for toccontent in toc.find_all_next(text=True):
cut_string = toccontent.split('|')
new_string = cut_string[0].lstrip().rstrip()
if new_string == '1' or new_string == '01':
logcount = 1
elif logcount > 0:
if new_string.isdigit():
logcount += 1
else:
break
totallogcount = totallogcount + logcount
if totallogcount > 0:
trackcount = totallogcount
logger.debug ('rutracker logtrackcount: %s' % totallogcount)
# If torrent track count = hp track count then return torrent,
# if greater, check for deluxe/special/foreign editions
# if less, then allow if it's a single track with a cue
valid = False
if trackcount == hptrackcount:
valid = True valid = True
elif trackcount > hptrackcount: else:
if any(deluxe in title for deluxe in deluxelist):
# Check torrent info
self.cookiejar.set_cookie(cookielib.Cookie(version=0, name='bb_dl', value=torrent_id, port=None, port_specified=False, domain='.rutracker.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False))
# Debug
#for cookie in self.cookiejar:
# logger.debug ('Cookie: %s' % cookie)
try:
page = self.opener.open(url)
torrent = page.read()
if torrent:
decoded = bdecode(torrent)
metainfo = decoded['info']
page.close ()
except Exception, e:
logger.error('Error getting torrent: %s' % e)
return False
# get torrent track count and check for cue
trackcount = 0
cuecount = 0
if 'files' in metainfo: # multi
for pathfile in metainfo['files']:
path = pathfile['path']
for file in path:
if any(file.lower().endswith('.' + x.lower()) for x in formatlist):
trackcount += 1
if '.cue' in file:
cuecount += 1
title = returntitle.lower()
logger.debug ('torrent title: %s' % title)
logger.debug ('headphones trackcount: %s' % hptrackcount)
logger.debug ('rutracker trackcount: %s' % trackcount)
# If torrent track count less than headphones track count, and there's a cue, then attempt to get track count from log(s)
# This is for the case where we have a single .flac/.wav which can be split by cue
# Not great, but shouldn't be doing this too often
totallogcount = 0
if trackcount < hptrackcount and cuecount > 0 and cuecount < hptrackcount:
page = self.opener.open(topicurl, timeout=60)
soup = BeautifulSoup(page.read())
findtoc = soup.find_all(text='TOC of the extracted CD')
if not findtoc:
findtoc = soup.find_all(text='TOC извлечённого CD')
for toc in findtoc:
logcount = 0
for toccontent in toc.find_all_next(text=True):
cut_string = toccontent.split('|')
new_string = cut_string[0].lstrip().rstrip()
if new_string == '1' or new_string == '01':
logcount = 1
elif logcount > 0:
if new_string.isdigit():
logcount += 1
else:
break
totallogcount = totallogcount + logcount
if totallogcount > 0:
trackcount = totallogcount
logger.debug ('rutracker logtrackcount: %s' % totallogcount)
# If torrent track count = hp track count then return torrent,
# if greater, check for deluxe/special/foreign editions
# if less, then allow if it's a single track with a cue
valid = False
if trackcount == hptrackcount:
valid = True valid = True
elif trackcount > hptrackcount:
if any(deluxe in title for deluxe in deluxelist):
valid = True
# Add to list # Add to list
if valid: if valid:
rulist.append((returntitle, size, topicurl)) rulist.append((returntitle, size, topicurl))
else: else:
if topicurl: if topicurl:
logger.info(u'<a href="%s">Torrent</a> found with %s tracks but the selected headphones release has %s tracks, skipping for rutracker.org' % (topicurl, trackcount, hptrackcount)) logger.info(u'<a href="%s">Torrent</a> found with %s tracks but the selected headphones release has %s tracks, skipping for rutracker.org' % (topicurl, trackcount, hptrackcount))
else: else:
logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (returntitle, int(size), int(seeders))) logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (returntitle, int(size), int(seeders)))
return rulist return rulist
def get_torrent(self, url, savelocation=None): def get_torrent(self, url, savelocation=None):
@@ -281,17 +280,70 @@ class Rutracker():
prev = os.umask(headphones.UMASK) prev = os.umask(headphones.UMASK)
page = self.opener.open(downloadurl) page = self.opener.open(downloadurl)
torrent = page.read() torrent = page.read()
decoded = bdecode(torrent)
metainfo = decoded['info']
tor_hash = sha1(bencode(metainfo)).hexdigest()
if savelocation: if savelocation:
download_path = os.path.join(savelocation, torrent_name) download_path = os.path.join(savelocation, torrent_name)
else: else:
tempdir = mkdtemp(suffix='_rutracker_torrents') tempdir = mkdtemp(suffix='_rutracker_torrents')
download_path = os.path.join(tempdir, torrent_name) download_path = os.path.join(tempdir, torrent_name)
fp = open (download_path, 'wb')
fp.write (torrent) with open(download_path, 'wb') as f:
fp.close () f.write(torrent)
os.umask(prev) os.umask(prev)
except Exception, e:
logger.error('Error getting torrent: %s' % e) # Add file to utorrent
if headphones.TORRENT_DOWNLOADER == 2:
self.utorrent_add_file(download_path)
except Exception as e:
logger.error('Error getting torrent: %s', e)
return False return False
return download_path return download_path, tor_hash
#TODO get this working in utorrent.py
def utorrent_add_file(self, filename):
host = headphones.UTORRENT_HOST
if not host.startswith('http'):
host = 'http://' + host
if host.endswith('/'):
host = host[:-1]
if host.endswith('/gui'):
host = host[:-4]
base_url = host
username = headphones.UTORRENT_USERNAME
password = headphones.UTORRENT_PASSWORD
session = requests.Session()
url = base_url + '/gui/'
session.auth = (username, password)
try:
r = session.get(url + 'token.html')
except Exception:
logger.exception('Error getting token')
return
if r.status_code == '401':
logger.debug('Error reaching utorrent')
return
regex = re.search(r'.+>([^<]+)</div></html>', r.text)
if regex is None:
logger.debug('Error reading token')
return
session.params = {'token': regex.group(1)}
with open(filename, 'rb') as f:
try:
session.post(url, params={'action': 'add-file'},
files={'torrent_file': f})
except Exception:
logger.exception('Error adding file to utorrent')
return

Some files were not shown because too many files have changed in this diff Show More