Compare commits

...
407 Commits
Author SHA1 Message Date
Pieter Janssens 9968bb2bb8 Merge pull request #1993 from rembo10/develop
Time for HP v0.5
2014-11-10 16:48:59 +01:00
Pieter Janssens e6397b28a5 Create CHANGELOG 2014-11-10 16:48:01 +01:00
Bas Stottelaar 61fa747813 Parsing RGID ID from folder is not second attempt in post processing steps, instead of third. See #1952 2014-11-10 13:26:18 +01:00
Bas Stottelaar a2ae89e5f9 Fix for logo 2014-11-10 12:53:45 +01:00
Bas Stottelaar 57f7bd1bd6 Updated README.md 2014-11-10 12:52:40 +01:00
Ade 47ed1e6a71 Searcher and torrent fixes
Should fix https://github.com/rembo10/headphones/issues/1990 and
https://github.com/rembo10/headphones/issues/1991
2014-11-08 08:33:45 +13:00
Ade 9d527e2fb2 Cuesplit fix up 2014-11-03 18:32:12 +13:00
Pieter Janssens 8e820cdad6 Merge pull request #1983 from jmullan/feature/travis-ci
Feature/travis ci
2014-11-02 14:07:04 +01:00
Jesse Mullan 0069bba318 Merge remote-tracking branch 'upstream/develop' into feature/travis-ci
* upstream/develop:
  Cue split config option

Conflicts:
	headphones/cuesplit.py
	headphones/postprocessor.py
	headphones/webserve.py
2014-11-01 20:46:52 -07:00
Jesse Mullan a67d0683ca Suppress a pylint error about something that we are handling 2014-11-01 19:49:24 -07:00
Ade 2b8c063c32 Cue split config option 2014-11-02 15:18:15 +13:00
Jesse Mullan 4ae3655025 Tell travis that we need pyopenssl 2014-11-01 19:10:29 -07:00
Jesse Mullan 29622ba221 Fix or suppress upgraded pylint errors 2014-11-01 18:25:00 -07:00
Jesse Mullan 45a7756828 Apparently there is no nosetests package in pip 2014-11-01 18:00:16 -07:00
Jesse Mullan 516c5fc84a Hopefully make pylint respect the pylintrc 2014-11-01 17:59:33 -07:00
Jesse Mullan 2ea34d4b97 Rearrange travis yaml file 2014-11-01 17:56:56 -07:00
Jesse Mullan 6952505368 Fix all pyflakes complaints 2014-11-01 17:50:30 -07:00
Jesse Mullan 60af816a66 Fix pyflakes complaints plus a pylint complaint 2014-11-01 17:03:11 -07:00
Jesse Mullan 364deea051 Fix pylint complaints 2014-11-01 17:02:56 -07:00
Jesse Mullan a300b62184 Loosen pep8 restrictions to not cover anything not fixed 2014-11-01 17:02:22 -07:00
Jesse Mullan 4e1716d58b Fix E712 comparison to True should be 'if cond is True:' or 'if cond:' 2014-11-01 16:33:35 -07:00
Jesse Mullan 7c98cbeb65 Fix E401 multiple imports on one line 2014-11-01 16:31:32 -07:00
Jesse Mullan 72852e04da Fix E721 do not compare types, use 'isinstance()' 2014-11-01 16:30:38 -07:00
Jesse Mullan 7726044c61 Fix whitespace around arithmetic operator 2014-11-01 16:28:44 -07:00
Jesse Mullan 76827abd78 Fix E701 multiple statements on one line (colon) 2014-11-01 16:27:02 -07:00
Jesse Mullan 561b6303bc Fix spaces after a semicolon 2014-11-01 16:26:16 -07:00
Jesse Mullan 4d85878921 Fix E703 statement ends with a semicolon 2014-11-01 16:24:42 -07:00
Jesse Mullan b7abdf1973 Fix E201,E202,E211,E241,E303 whitespace issues 2014-11-01 16:23:38 -07:00
Jesse Mullan 015d269667 Fix E711 comparison to None should be if cond is None: 2014-11-01 16:22:05 -07:00
Jesse Mullan 5ede29b401 Fix W601 .has_key() is deprecated 2014-11-01 16:20:43 -07:00
Jesse Mullan 4895d562b3 Ignore various pep8 comments rules 2014-11-01 16:19:55 -07:00
Jesse Mullan 5215e55848 Merge branch 'feature/autopep8' into feature/travis-ci
* feature/autopep8:
  Fix more config saving options
  autopep8 E272 whitespace before multiple spaces before keyword
  manual pep8 W291,W293,W391 blank lines, trailing whitespace
  autopep8 E221,E222,E225,E227,E228,E251 spaces around operators and in parameters
  autopep8 E301,E302,E303 too many / too few blank lines
  autopep8 E203 whitespace before colon
  autopep8 E231 whitespace after some punctuation
  autopep8 api.py
  autopep8 albumswitcher
  autopep8 headphones/albumart.py
  autopep8 headphones/__init__.py
  autopep8 Headphones.py
2014-11-01 16:16:58 -07:00
Jesse Mullan f8750645c2 Merge remote-tracking branch 'origin/develop' into feature/autopep8
* origin/develop:
  Fix more config saving options
2014-11-01 16:16:24 -07:00
AdeHub 9b6d75f370 Merge pull request #1982 from jmullan/feature/fix_more_config_saving
Fix more config saving options
2014-11-02 12:06:33 +13:00
Jesse Mullan c9f72d984e Add pep8 config file 2014-11-01 16:05:27 -07:00
Jesse Mullan 48022d92fe Set up basic travis ci testing 2014-11-01 16:00:53 -07:00
Jesse Mullan 1732205174 Merge remote-tracking branch 'origin/develop' into feature/autopep8
* origin/develop:
  Fix various config keys and template config keys to round trip more data
  Fix global of CURRENT_VERSION + other things that pylint points out
  Fix argument to turn SSL cert checking on and off

Conflicts:
	headphones/__init__.py
	headphones/webserve.py
2014-11-01 15:42:32 -07:00
Jesse Mullan 32e57ade15 Fix more config saving options 2014-11-01 15:29:57 -07:00
Bas Stottelaar b8cd8bd31b Merge pull request #1972 from jmullan/feature/fix_config_form
Fix various config keys and template config keys
2014-10-29 17:02:31 +01:00
Jesse Mullan b0c7c8b682 Fix various config keys and template config keys to round trip more data 2014-10-29 07:13:44 -07:00
AdeHub 4af96b2bb6 Merge pull request #1969 from jmullan/feature/fix_CURRENT_VERSION
Fix global of CURRENT_VERSION + other things that pylint points out
2014-10-29 07:37:52 +13:00
Jesse Mullan 8d7fd835a8 Fix global of CURRENT_VERSION + other things that pylint points out 2014-10-28 11:22:43 -07:00
AdeHub 56beb897cf Merge pull request #1965 from jmullan/feature/fix_ssl_check
Fix argument to turn SSL cert checking on and off
2014-10-28 07:11:36 +13:00
Jesse Mullan 3bcea4b692 autopep8 E272 whitespace before multiple spaces before keyword 2014-10-27 11:06:32 -07:00
Jesse Mullan 7369311501 manual pep8 W291,W293,W391 blank lines, trailing whitespace 2014-10-27 11:03:41 -07:00
Jesse Mullan 9eef7e40f5 autopep8 E221,E222,E225,E227,E228,E251 spaces around operators and in parameters 2014-10-27 11:00:48 -07:00
Jesse Mullan 5a4bff5be8 autopep8 E301,E302,E303 too many / too few blank lines 2014-10-27 10:56:43 -07:00
Jesse Mullan a040f38a3f autopep8 E203 whitespace before colon 2014-10-27 10:54:57 -07:00
Jesse Mullan 3ff09aeb46 autopep8 E231 whitespace after some punctuation 2014-10-27 10:53:47 -07:00
Jesse Mullan 045f1cd766 autopep8 api.py 2014-10-27 10:49:09 -07:00
Jesse Mullan c691b7c39d autopep8 albumswitcher 2014-10-27 10:48:15 -07:00
Jesse Mullan ed21bd4b3e autopep8 headphones/albumart.py 2014-10-27 10:47:36 -07:00
Jesse Mullan be7716326e autopep8 headphones/__init__.py 2014-10-27 10:47:05 -07:00
Jesse Mullan 8c21f781e1 autopep8 Headphones.py 2014-10-27 10:46:45 -07:00
Jesse Mullan 5c1223adfb Fix argument to turn SSL cert checking on and off 2014-10-27 10:16:43 -07:00
AdeHub 23055da02d Merge pull request #1955 from jmullan/feature/refactor_config
Reduce headphones module-level variables and reduce usage of globals, refactor processing of config from web form.
2014-10-27 18:48:04 +13:00
Ade 20981a6c61 Cue split part3 2014-10-27 17:30:22 +13:00
Jesse Mullan b082fbc0a5 Make sure that old config data is all written out 2014-10-26 09:09:12 -07:00
Jesse Mullan 2af45fd3fe Fix new variables 2014-10-26 08:49:09 -07:00
Ade 7d621cbc28 Cue split part 2 2014-10-26 19:00:09 +13:00
Jesse Mullan 48ef8a93a9 Merge remote-tracking branch 'upstream/develop' into feature/refactor_config
Conflicts:
	headphones/postprocessor.py
2014-10-25 19:14:14 -07:00
Jesse Mullan 80d0c6d430 Rename headphones.CFG to headphones CONFIG 2014-10-25 11:40:56 -07:00
Jesse Mullan ada8603e9f Fix extras form data; change bool cast to int(bool( cast for some data 2014-10-25 11:39:32 -07:00
Ade ce73883ba2 Auto cue split
- Attempt to split audio files by cue sheet during post processing,
requires shntools with flac or xld. If the split fails then the
directory is set as unprocessed.
https://github.com/rembo10/headphones/issues/1938

- last.fm album art/info - try with release id if adding album
manually. https://github.com/rembo10/headphones/issues/1923,
https://github.com/rembo10/headphones/issues/1871
2014-10-25 16:32:53 +13:00
Jesse Mullan bfbc2e3488 Merge remote-tracking branch 'upstream/develop' into feature/refactor_config 2014-10-23 11:59:04 -07:00
AdeHub ef493f65a0 Merge pull request #1959 from basilfx/symlinkScan
Follow symlinks for library scan.
2014-10-23 18:59:26 +13:00
Jesse Mullan d76fce8cbc Merge remote-tracking branch 'upstream/develop' into feature/refactor_config
Conflicts:
	headphones/__init__.py
	headphones/searcher.py
	headphones/webserve.py
2014-10-22 10:02:18 -07:00
Bas Stottelaar 87b444684a Follow symlinks for music folder scan. Fixes #1953 2014-10-22 11:35:00 +02:00
AdeHub f9dfdb2c98 Merge pull request #1958 from basilfx/improvements
Various improvements
2014-10-22 16:48:10 +13:00
Bas Stottelaar fd7bb4eb7e Add missing library for APScheduler. 2014-10-21 02:54:13 +02:00
Bas Stottelaar 2ddb96e134 Clarified post processing logging. 2014-10-21 02:43:50 +02:00
Bas Stottelaar 5ebe6bcd00 Display query/type in search box on results page. 2014-10-21 02:33:41 +02:00
Bas Stottelaar 6b4afd8877 Cleanup search code. Add support for magnet links conversion. Fixes #1926. 2014-10-21 02:07:56 +02:00
Bas Stottelaar 8a8821530a Jump back to top when changing pagination page. Fixes #1948 2014-10-21 00:22:53 +02:00
Bas Stottelaar bea7022b4c Upgrade autoreload setting 2014-10-20 21:48:17 +02:00
Bas Stottelaar 553cc536c0 Upgraded APScheduler to 3.0.1. This includes pytz and tzlocal as dependencies. 2014-10-20 21:48:10 +02:00
Jesse Mullan 8fbef76dc0 Merge remote-tracking branch 'upstream/develop' into feature/refactor_config 2014-10-20 00:13:02 -07:00
Jesse Mullan 2686b4c190 Remove superfluous values from configuration tuples 2014-10-20 00:12:56 -07:00
Jesse Mullan 43109f8172 Fix djmix to be dj-mix 2014-10-19 14:32:38 -07:00
AdeHub cf181e1d26 Merge pull request #1951 from andrenam/cherrypy360
Upgrade Cherrypy to version 3.6.0
2014-10-18 08:14:08 +13:00
andrenam 14c6f68fb7 - upgrade cherrypy to version 3.6.0
- dont't pass unicode hostname to cherrypy (see cherrypy-issue #1285,
https://bitbucket.org/cherrypy/cherrypy/issue/1285/n-must-be-a-native-str-got-unicode)
2014-10-14 02:43:55 +02:00
AdeHub 47f93d97d5 Merge pull request #1947 from basilfx/repocleanup
Updated the basic repository files. Structured API documentation, added ...
2014-10-11 10:53:35 +13:00
Jesse Mullan 0ffbd7824d Saving the config appears to work again 2014-10-09 10:51:03 -07:00
Bas Stottelaar 5ea6ec4328 Updated the basic repository files. Structured API documentation, added CONTRIBUTING.md 2014-10-08 20:57:43 +02:00
Ade 6b4fbe002d Search Results - Allow filtering by mb release id 2014-10-03 17:54:57 +13:00
Ade 7a078dc880 Manually adding release - update have
If the album has been scanned before adding the release it will be
unmatched, update to matched
https://github.com/rembo10/headphones/issues/1586
2014-10-02 15:51:20 +13:00
Ade 63e106a12d And another attempt to normalize 2014-10-01 22:13:02 +13:00
Ade 63a32e473f Autowant manually added albums config option 2014-09-27 15:31:33 +12:00
Ade e07fafda6e Don't auto search for manually added VA albums 2014-09-27 09:40:31 +12:00
AdeHub d887d06a4f Merge pull request #1919 from basilfx/fixes
Several fixes for some issues.
2014-09-26 07:15:11 +12:00
Bas Stottelaar 8d839ef3cf Fix unicode characters in Growl messages. Fixes #1695 2014-09-25 15:16:05 +02:00
Bas Stottelaar 353be0dbf5 Open file for writing instead of os.access. Fix for #1752 2014-09-25 14:31:47 +02:00
Bas Stottelaar 05ce5f7853 Latest Album -> Latest Release, fixes #1845 2014-09-25 14:13:33 +02:00
Bas Stottelaar f520ceaf25 Catch IOError. Fixes #1911 2014-09-25 14:11:57 +02:00
Bas Stottelaar 371401fa36 Updated requests library to 2.4.1. Should fix #1915 2014-09-25 14:08:27 +02:00
Ade 574a76c231 Normalize path replacement strings part 2
Potential fix for https://github.com/rembo10/headphones/issues/1824
2014-09-25 18:48:23 +12:00
Ade f6bb76acb0 Merge branch 'master' into develop 2014-09-23 17:53:53 +12:00
Ade 3f5044c82d OrderedDict fix 2014-09-23 17:44:56 +12:00
Ade 394876992b Lyrics mods
Switched to beets lyrics plugin
Fixes https://github.com/rembo10/headphones/issues/1896
2014-09-22 15:38:17 +12:00
AdeHub 365d4fa1bf Merge pull request #1907 from basilfx/ratelimit2
Increase Last.FM rate limit to 5 req/sec. Improved rate limiting
2014-09-22 11:39:23 +12:00
Bas Stottelaar 66309d806a Increase Last.FM rate limit to 5 req/sec. Improved rate limiting 2014-09-22 00:47:52 +02:00
AdeHub e7a97ed277 Merge pull request #1906 from basilfx/ratelimit
Initial version of rate limiting on requests
2014-09-22 10:16:26 +12:00
Bas Stottelaar c09c290ac8 Request should be in the lock 2014-09-21 18:44:02 +02:00
Bas Stottelaar efdd51c361 Don't load all Last.FM hits at once 2014-09-21 14:05:01 +02:00
Bas Stottelaar f6346f3c41 Save last request time after request itself. 2014-09-21 13:52:06 +02:00
Bas Stottelaar 2c72fb8e8e Initial version of rate limiting on requests. Should fix issue #1877 2014-09-21 13:42:11 +02:00
AdeHub 0c9de181f8 Merge pull request #1904 from basilfx/bugfixes8
Fixes for issues
2014-09-21 11:04:49 +12:00
Ade 757ebe9085 Merge branch 'master' into develop 2014-09-21 10:57:46 +12:00
Ade 61dc4fa95e KAT proxy fix 2014-09-21 10:56:27 +12:00
Bas Stottelaar c05fa78fa0 Fix for #1902 2014-09-21 00:55:35 +02:00
Bas Stottelaar 6a16fc2526 Fix for #1903 2014-09-21 00:52:59 +02:00
AdeHub e790443829 Merge pull request #1901 from rembo10/develop
Develop
2014-09-21 00:55:38 +12:00
AdeHub 9c1a0bc151 Merge pull request #1900 from basilfx/mpfix
OSError instead of IOError
2014-09-21 00:19:22 +12:00
Bas Stottelaar 1379ff7845 Oops, it should be an OSError instead of IOError 2014-09-20 13:18:14 +02:00
AdeHub 3a13612d15 Merge pull request #1899 from basilfx/mpfix
Multiprocessing IOError fixes
2014-09-20 23:12:21 +12:00
Bas Stottelaar 5305b6a3a8 Check for permission errors in queues and defer queue initialization until use. Fixes #1897. 2014-09-20 13:03:49 +02:00
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
922 changed files with 31887 additions and 29504 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto
+9 -2
View File
@@ -28,8 +28,12 @@ ehthumbs.db
Icon?
Thumbs.db
#ignore thumbnails created by windows
Thumbs.db
#Ignore files generated by PyCharm
.idea/*
#Ignore files generated by vi
*.swp
#Ignore files build by Visual Studio
*.obj
*.exe
@@ -60,3 +64,6 @@ _ReSharper*/
/logs
.project
.pydevproject
headphones_docs
+17
View File
@@ -0,0 +1,17 @@
[pep8]
# E111 indentation is not a multiple of four
# E121 continuation line under-indented for hanging indent
# E122 continuation line missing indentation or outdented
# E124 closing bracket does not match visual indentation
# E125 continuation line with same indent as next logical line
# E126 continuation line over-indented for hanging indent
# E127 continuation line over-indented for visual indent
# E128 continuation line under-indented for visual indent
# E261 at least two spaces before inline comment
# E262 inline comment should start with '# '
# E265 block comment should start with '# '
# E302 expected 2 blank lines, found 1
# E501 line too long (312 > 160 characters)
# E502 the backslash is redundant between brackets
ignore = E111,E121,E122,E123,E124,E125,E126,E127,E128,E261,E262,E265,E302,E501,E502
max-line-length = 160
+20
View File
@@ -0,0 +1,20 @@
# Travis CI configuration file
# http://about.travis-ci.org/docs/
language: python
# Available Python versions:
# http://about.travis-ci.org/docs/user/ci-environment/#Python-VM-images
python:
- "2.6"
- "2.7"
install:
- pip install pyOpenSSL
- pip install pylint
- pip install pyflakes
- pip install pep8
script:
- pep8 headphones
- pylint --rcfile=pylintrc headphones
- pyflakes headphones
- nosetests headphones
+109
View File
@@ -0,0 +1,109 @@
# API Reference
The API is still pretty new and needs some serious cleaning up on the backend but should be reasonably functional. There are no error codes yet.
## General structure
The API endpoint is `http://ip:port + HTTP_ROOT + /api?apikey=$apikey&cmd=$command`
Data response in JSON formatted. If executing a command like "delArtist" or "addArtist" you'll get back an "OK", else, you'll get the data you requested.
## API methods
### getIndex
Fetch data from index page. Returns: ArtistName, ArtistSortName, ArtistID, Status, DateAdded, [LatestAlbum, ReleaseDate, AlbumID], HaveTracks, TotalTracks, IncludeExtras, LastUpdated, [ArtworkURL, ThumbURL]: a remote url to the artwork/thumbnail.
To get the cached image path, see getArtistArt command. ThumbURL is added/updated when an artist is added/updated. If your using the database method to get the artwork, it's more reliable to use the ThumbURL than the ArtworkURL)
### getArtist&id=$artistid
Fetch artist data. returns the artist object (see above) and album info: Status, AlbumASIN, DateAdded, AlbumTitle, ArtistName, ReleaseDate, AlbumID, ArtistID, Type, ArtworkURL: hosted image path. For cached image, see getAlbumArt command)
### getAlbum&id=$albumid
Fetch data from album page. Returns the album object, a description object and a tracks object. Tracks contain: AlbumASIN, AlbumTitle, TrackID, Format, TrackDuration (ms), ArtistName, TrackTitle, AlbumID, ArtistID, Location, TrackNumber, CleanName (stripped of punctuation /styling), BitRate)
### getUpcoming
Returns: Status, AlbumASIN, DateAdded, AlbumTitle, ArtistName, ReleaseDate, AlbumID, ArtistID, Type
### getWanted
Returns: Status, AlbumASIN, DateAdded, AlbumTitle, ArtistName, ReleaseDate, AlbumID, ArtistID, Type
### getSimilar
Returns similar artists - with a higher "Count" being more likely to be similar. Returns: Count, ArtistName, ArtistID
### getHistory
Returns: Status, DateAdded, Title, URL (nzb), FolderName, AlbumID, Size (bytes)
### getLogs
Not working yet
### findArtist&name=$artistname[&limit=$limit]
Perform artist query on musicbrainz. Returns: url, score, name, uniquename (contains disambiguation info), id)
### findAlbum&name=$albumname[&limit=$limit]
Perform album query on musicbrainz. Returns: title, url (artist), id (artist), albumurl, albumid, score, uniquename (artist - with disambiguation)
### addArtist&id=$artistid
Add an artist to the db by artistid)
### addAlbum&id=$releaseid
Add an album to the db by album release id
### delArtist&id=$artistid
Delete artist from db by artistid)
### pauseArtist&id=$artistid
Pause an artist in db)
### resumeArtist&id=$artistid
Resume an artist in db)
### refreshArtist&id=$artistid
Refresh info for artist in db from musicbrainz
### queueAlbum&id=$albumid[&new=True&lossless=True]
Mark an album as wanted and start the searcher. Optional paramters: 'new' looks for new versions, 'lossless' looks only for lossless versions
### unqueueAlbum&id=$albumid
Unmark album as wanted / i.e. mark as skipped
### forceSearch
force search for wanted albums - not launched in a separate thread so it may take a bit to complete
### forceProcess
Force post process albums in download directory - also not launched in a separate thread
### getVersion
Returns some version information: git_path, install_type, current_version, installed_version, commits_behind
### checkGithub
Updates the version information above and returns getVersion data
### shutdown
Shut down headphones
### restart
Restart headphones
### update
Update headphones - you may want to check the install type in get version and not allow this if type==exe
### getArtistArt&id=$artistid
Returns either a relative path to the cached image, or a remote url if the image can't be saved to the cache dir
getAlbumArt&id=$albumid
see above
### getArtistInfo&id=$artistid
Returns Summary and Content, both formatted in html.
### getAlbumInfo&id=$albumid
See above, returns Summary and Content.
### getArtistThumb&id=$artistid
Returns either a relative path to the cached thumbnail artist image, or an http:// address if the cache dir can't be written to.
### getAlbumThumb&id=$albumid
See above.
### choose_specific_download&id=$albumid
Gives you a list of results from searcher.searchforalbum(). Basically runs a normal search, but rather than sorting them and downloading the best result, it dumps the data, which you can then pass on to download_specific_release(). Returns a list of dictionaries with params: title, size, url, provider & kind - all of these values must be passed back to download_specific_release
### download_specific_release&id=albumid&title=$title&size=$size&url=$url&provider=$provider&kind=$kind
Allows you to manually pass a choose_specific_download release back to searcher.send_to_downloader()
-71
View File
@@ -1,71 +0,0 @@
The API is still pretty new and needs some serious cleaning up on the backend but should be
reasonably functional. There are no error codes yet,
General structure:
http://localhost:8181 + HTTP_ROOT + /api?apikey=$apikey&cmd=$command
Data returned in json format. If executing a command like "delArtist" or "addArtist" you'll get back an "OK", else, you'll get the data you requested
$commands&parameters[&optionalparameters]:
getIndex (fetch data from index page. Returns: ArtistName, ArtistSortName, ArtistID, Status, DateAdded,
[LatestAlbum, ReleaseDate, AlbumID], HaveTracks, TotalTracks,
IncludeExtras, LastUpdated, [ArtworkURL, ThumbURL]: a remote url to the artwork/thumbnail. To get the cached image path, see getArtistArt command.
ThumbURL is added/updated when an artist is added/updated. If your using the database method to get the artwork,
it's more reliable to use the ThumbURL than the ArtworkURL)
getArtist&id=$artistid (fetch artist data. returns the artist object (see above) and album info: Status, AlbumASIN, DateAdded, AlbumTitle, ArtistName, ReleaseDate, AlbumID, ArtistID, Type, ArtworkURL: hosted image path. For cached image, see getAlbumArt command)
getAlbum&id=$albumid (fetch data from album page. Returns the album object, a description object and a tracks object. Tracks contain: AlbumASIN, AlbumTitle, TrackID, Format, TrackDuration (ms), ArtistName, TrackTitle, AlbumID, ArtistID, Location, TrackNumber, CleanName (stripped of punctuation /styling), BitRate)
getUpcoming (Returns: Status, AlbumASIN, DateAdded, AlbumTitle, ArtistName, ReleaseDate, AlbumID, ArtistID, Type)
getWanted (Returns: Status, AlbumASIN, DateAdded, AlbumTitle, ArtistName, ReleaseDate, AlbumID, ArtistID, Type)
getSimilar (Returns similar artists - with a higher "Count" being more likely to be similar. Returns: Count, ArtistName, ArtistID)
getHistory (Returns: Status, DateAdded, Title, URL (nzb), FolderName, AlbumID, Size (bytes))
getLogs (not working yet)
findArtist&name=$artistname[&limit=$limit] (perform artist query on musicbrainz. Returns: url, score, name, uniquename (contains disambiguation info), id)
findAlbum&name=$albumname[&limit=$limit] (perform album query on musicbrainz. Returns: title, url (artist), id (artist), albumurl, albumid, score, uniquename (artist - with disambiguation)
addArtist&id=$artistid (add an artist to the db by artistid)
addAlbum&id=$releaseid (add an album to the db by album release id)
delArtist&id=$artistid (delete artist from db by artistid)
pauseArtist&id=$artistid (pause an artist in db)
resumeArtist&id=$artistid (resume an artist in db)
refreshArtist&id=$artistid (refresh info for artist in db from musicbrainz)
queueAlbum&id=$albumid[&new=True&lossless=True] (Mark an album as wanted and start the searcher. Optional paramters: 'new' looks for new versions, 'lossless' looks only for lossless versions)
unqueueAlbum&id=$albumid (Unmark album as wanted / i.e. mark as skipped)
forceSearch (force search for wanted albums - not launched in a separate thread so it may take a bit to complete)
forceProcess (force post process albums in download directory - also not launched in a separate thread)
getVersion (Returns some version information: git_path, install_type, current_version, installed_version, commits_behind
checkGithub (updates the version information above and returns getVersion data)
shutdown (shut down headphones)
restart (restart headphones)
update (update headphones - you may want to check the install type in get version and not allow this if type==exe)
getArtistArt&id=$artistid (Returns either a relative path to the cached image, or a remote url if the image can't be saved to the cache dir)
getAlbumArt&id=$albumid (see above)
getArtistInfo&id=$artistid (Returns Summary and Content, both formatted in html)
getAlbumInfo&id=$albumid (See above, returns Summary and Content)
getArtistThumb&id=$artistid (Returns either a relative path to the cached thumbnail artist image, or an http:// address if the cache dir can't be written to)
getAlbumThumb&id=$albumid (see above)
choose_specific_download&id=$albumid (Gives you a list of results from searcher.searchforalbum(). Basically runs a normal search, but rather than sorting them and downloading the best result, it dumps the data, which you can then pass on to download_specific_release(). Returns a list of dictionaries with params: title, size, url, provider & kind - all of these values must be passed back to download_specific_release)
download_specific_release&id=albumid&title=$title&size=$size&url=$url&provider=$provider&kind=$kind (Allows you to manually pass a choose_specific_download release back to searcher.send_to_downloader())
+7
View File
@@ -0,0 +1,7 @@
v0.5 Released 10 Nov 2014
-------------------------
- Several bug fixes (please retest your posted issues)
- Cue splitter
- Other improvements
+35
View File
@@ -0,0 +1,35 @@
# Contributing to Headphones
## For users
In case you read this because you are posting an issue, please take a minute and conside the things below. The issue tracker is not a support forum. It is primarily intended to submit bugs, improvements or feature requests. However, we are glad to help you, and make sure the problem is not caused by Headphones, but don't expect step-by-step answers.
* Use the search function. Chances are that your problem is already discussed.
* Visit the [Troubleshooting](../../wiki/TroubleShooting) wiki first.
* Use [proper formatting](https://help.github.com/articles/github-flavored-markdown/). Paste your logs in code blocks.
* Close your issue if you resolved it.
## For developers
If you think you can contribute code to the Headphones repository, do not hesitate to submit a pull request.
### Branches
All pull requests should be based on the `develop` branch. When you want to develop a new feature, clone the repository with `git clone origin/develop -b FEATURE_NAME`. Use meaningful commit messages.
### Code compatibility
The code should work with Python 2.6 and 2.7. Note that Headphones runs on different platforms, including Network Attached Storage devices such as Synology.
Re-use existing code. Do not hesitate to add logging in your code. You can the logger module `headphones.logger.*` for this. Web requests are invoked via `headphones.request.*` and derived ones. Use these methods to automatically add proper and meaningful error handling.
### Code conventions
Altough Headphones did not adapt a code convention in the past, we try to follow the [PEP8](http://legacy.python.org/dev/peps/pep-0008/) conventions for future code. A short summary to remind you (copied from http://wiki.ros.org/PyStyleGuide):
* 4 space indentation
* 80 characters per line
* `package_name`
* `ClassName`
* `method_name`
* `field_name`
* `_private_something`
* `self.__really_private_field`
* `_global`
Document your code!
+82 -65
View File
@@ -14,31 +14,31 @@
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
import os, sys
import os
import sys
# Ensure lib added to path, before any other imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
from headphones import webstart, logger
import locale
import time
import signal
from lib.configobj import ConfigObj
import argparse
import headphones
from headphones import webstart, logger
try:
import argparse
except ImportError:
import lib.argparse as argparse
# Register signals, such as CTRL + C
signal.signal(signal.SIGINT, headphones.sig_handler)
signal.signal(signal.SIGTERM, headphones.sig_handler)
def main():
"""
Headphones application entry point. Parses arguments, setups encoding and
initializes the application.
"""
# Fixed paths to Headphones
if hasattr(sys, 'frozen'):
headphones.FULL_PATH = os.path.abspath(sys.executable)
@@ -63,112 +63,129 @@ def main():
headphones.SYS_ENCODING = 'UTF-8'
# 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('-q', '--quiet', action='store_true', help='Turn off console logging')
parser.add_argument('-d', '--daemon', action='store_true', help='Run as a daemon')
parser.add_argument('-p', '--port', type=int, help='Force Headphones to run on a specified port')
parser.add_argument('--datadir', help='Specify a directory where to store your data files')
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(
'-d', '--daemon', action='store_true', help='Run as a daemon')
parser.add_argument(
'-p', '--port', type=int, help='Force Headphones to run on a specified port')
parser.add_argument(
'--datadir', help='Specify a directory where to store your data files')
parser.add_argument('--config', help='Specify a config file to use')
parser.add_argument('--nolaunch', action='store_true', help='Prevent browser from launching on startup')
parser.add_argument('--pidfile', help='Create a pid file (only relevant when running as a daemon)')
parser.add_argument('--nolaunch', action='store_true',
help='Prevent browser from launching on startup')
parser.add_argument(
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
args = parser.parse_args()
if args.verbose:
headphones.VERBOSE = 2
elif args.quiet:
headphones.VERBOSE = 0
headphones.VERBOSE = True
if args.quiet:
headphones.QUIET = True
if args.daemon:
if sys.platform == 'win32':
print "Daemonize not supported under Windows, starting normally"
sys.stderr.write(
"Daemonizing not supported under Windows, starting normally\n")
else:
headphones.DAEMON=True
headphones.VERBOSE = False
headphones.DAEMON = True
headphones.QUIET = True
if args.pidfile:
headphones.PIDFILE = str(args.pidfile)
# If the pidfile already exists, headphones may still be running, so exit
# If the pidfile already exists, headphones may still be running, so
# exit
if os.path.exists(headphones.PIDFILE):
sys.exit("PID file '" + headphones.PIDFILE + "' already exists. Exiting.")
sys.exit(
"PID file '" + headphones.PIDFILE + "' already exists. Exiting.")
# 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:
headphones.CREATEPID = True
try:
file(headphones.PIDFILE, 'w').write("pid\n")
except IOError, e:
raise SystemExit("Unable to write PID file: %s [%d]" % (e.strerror, e.errno))
else:
logger.warn("Not running in daemon mode. PID file creation disabled.")
try:
with open(headphones.PIDFILE, 'w') as fp:
fp.write("pid\n")
except IOError as e:
raise SystemExit(
"Unable to write PID file: %s [%d]", e.strerror, e.errno)
else:
logger.warn(
"Not running in daemon mode. PID file creation disabled.")
# Determine which data directory and config file to use
if args.datadir:
headphones.DATA_DIR = args.datadir
else:
headphones.DATA_DIR = headphones.PROG_DIR
if args.config:
headphones.CONFIG_FILE = args.config
config_file = args.config
else:
headphones.CONFIG_FILE = os.path.join(headphones.DATA_DIR, 'config.ini')
config_file = os.path.join(headphones.DATA_DIR, 'config.ini')
# Try to create the DATA_DIR if it doesn't exist
if not os.path.exists(headphones.DATA_DIR):
try:
os.makedirs(headphones.DATA_DIR)
except OSError:
raise SystemExit('Could not create data directory: ' + headphones.DATA_DIR + '. Exiting....')
raise SystemExit(
'Could not create data directory: ' + headphones.DATA_DIR + '. Exiting....')
# Make sure the DATA_DIR is writeable
if not os.access(headphones.DATA_DIR, os.W_OK):
raise SystemExit('Cannot write to the data directory: ' + headphones.DATA_DIR + '. Exiting...')
raise SystemExit(
'Cannot write to the data directory: ' + headphones.DATA_DIR + '. Exiting...')
# Put the database in the DATA_DIR
headphones.DB_FILE = os.path.join(headphones.DATA_DIR, 'headphones.db')
headphones.CFG = ConfigObj(headphones.CONFIG_FILE, encoding='utf-8')
# Read config & start logging
headphones.initialize()
# Read config and start logging
headphones.initialize(config_file)
if headphones.DAEMON:
if sys.platform == "win32":
print "Daemonize not supported under Windows, starting normally"
else:
headphones.daemonize()
headphones.daemonize()
#configure the connection to the musicbrainz database
# Configure the connection to the musicbrainz database
headphones.mb.startmb()
# Force the http port if neccessary
if 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:
http_port = int(headphones.HTTP_PORT)
http_port = int(headphones.CONFIG.HTTP_PORT)
# Try to start the server.
webstart.initialize({
'http_port': http_port,
'http_host': headphones.HTTP_HOST,
'http_root': headphones.HTTP_ROOT,
'http_proxy': headphones.HTTP_PROXY,
'enable_https': headphones.ENABLE_HTTPS,
'https_cert': headphones.HTTPS_CERT,
'https_key': headphones.HTTPS_KEY,
'http_username': headphones.HTTP_USERNAME,
'http_password': headphones.HTTP_PASSWORD,
})
# Try to start the server. Will exit here is address is already in use.
web_config = {
'http_port': http_port,
'http_host': headphones.CONFIG.HTTP_HOST,
'http_root': headphones.CONFIG.HTTP_ROOT,
'http_proxy': headphones.CONFIG.HTTP_PROXY,
'enable_https': headphones.CONFIG.ENABLE_HTTPS,
'https_cert': headphones.CONFIG.HTTPS_CERT,
'https_key': headphones.CONFIG.HTTPS_KEY,
'http_username': headphones.CONFIG.HTTP_USERNAME,
'http_password': headphones.CONFIG.HTTP_PASSWORD,
}
webstart.initialize(web_config)
if headphones.LAUNCH_BROWSER and not args.nolaunch:
headphones.launch_browser(headphones.HTTP_HOST, http_port, headphones.HTTP_ROOT)
if headphones.CONFIG.LAUNCH_BROWSER and not args.nolaunch:
headphones.launch_browser(headphones.CONFIG.HTTP_HOST, http_port,
headphones.CONFIG.HTTP_ROOT)
# Start the background threads
headphones.start()
# Wait endlessy for a signal to happen
while True:
if not headphones.SIGNAL:
try:
@@ -177,6 +194,7 @@ def main():
headphones.SIGNAL = 'shutdown'
else:
logger.info('Received signal: %s', headphones.SIGNAL)
if headphones.SIGNAL == 'shutdown':
headphones.shutdown()
elif headphones.SIGNAL == 'restart':
@@ -186,7 +204,6 @@ def main():
headphones.SIGNAL = None
return
# Call main()
if __name__ == "__main__":
main()
View File
+64 -44
View File
@@ -1,44 +1,64 @@
#![preview thumb](https://github.com/rembo10/headphones/raw/master/data/images/headphoneslogo.png)Headphones
###Installation and Notes
This is a relatively early release of a third-party add-on for SABnzbd. It's been around for about a year, and while
quite functional, there are still a lot of kinks to work out!
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'.
For additional startup options, type 'python Headphones.py -h' or 'python Headphones.py --help'
###Screenshots
Homepage (Artist Overview)
![preview thumb](http://i.imgur.com/LZO9a.png)
One of the many settings pages....
![preview thumb](http://i.imgur.com/xcWNy.png)
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)
If you run into any issues, use the GitHub issue tracker at: http://github.com/rembo10/headphones/issues.
This is free software under the GPL v3 open source license - so feel free to do with it what you wish.
#![Headphones Logo](https://github.com/rembo10/headphones/raw/master/data/images/headphoneslogo.png) Headphones
Headphones is an automated music downloader for NZB and Torrent, written in Python. It supports SABnzbd, NZBget, Transmission, µTorrent and Blackhole.
## Support & Discuss
You are free to join the Headphones support community on IRC where you can ask questions, hang around and discuss anything related to HP.
1. Use any IRC client and connect to the Freenode server, `irc.freenode.net`.
2. Join the `#headphones` channel.
## Installation and Notes
* [Installation page](../../wiki/Usage-guide) shows you how to install Headphones.
* [Usage guide](../../wiki/Usage-guide) introduces you to Headphones.
* [Troubleshooting page](../../wiki/TroubleShooting) in the wiki can help you with common problems.
**Issues** can be reported on the GitHub issue tracker considering these rules:
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.
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.
**Feature requests** can be reported on the GitHub issue tracker too:
1. Search for similar existing 'issues', feature requests can be recognized by the label 'Request'.
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.
If you **comply with these rules** you can [post your request/issue](http://github.com/rembo10/headphones/issues).
**Support** the project by implementing new features, solving support tickets and provide bug fixes.
## Screenshots
Homepage (Artist Overview):
![preview thumb](http://i.imgur.com/LZO9a.png)
One of the many settings pages:
![preview thumb](http://i.imgur.com/xcWNy.png)
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)
Search Results:
![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)
## License
This is free software under the GPL v3 open source license. Feel free to do with it what you wish, but any modification must be open sourced. A copy of the license is included.
-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">
<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>
%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>
@@ -89,9 +89,15 @@
<div id="albumImg">
<img height="200" alt="" class="albumArt" src="artwork/album/${album['AlbumID']}">
</div>
<h1><a href="http://musicbrainz.org/release-group/${album['AlbumID']}">${album['AlbumTitle']}</a></h1>
<h2><a href="http://musicbrainz.org/artist/${album['ArtistID']}">${album['ArtistName']}</a></h2>
<h1 id="albumname">
<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]
totaltracks = len(myDB.select("SELECT TrackTitle from tracks WHERE AlbumID=?", [album['AlbumID']]))
@@ -275,9 +281,58 @@
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() {
getAlbumInfo();
initThisPage();
checkAlbumStatus();
refreshInterval = setInterval(function(){
checkAlbumStatus();
}, 3000);
});
</script>
+87 -59
View File
@@ -8,7 +8,7 @@
<%def name="headerIncludes()">
<div id="subhead_container">
<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>
%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>
@@ -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>
%endif
%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>
%else:
<a id="menu_link_getextra" href="#"><i class="fa fa-plus"></i> Get Extras</a>
%endif
<div id="dialog" title="Choose Which Extras to Fetch" style="display:none" class="configtable">
<form action="getExtras" method="get" class="form">
<input type="hidden" name="ArtistID" value="${artist['ArtistID']}">
<input type="hidden" name="newstyle" value="true">
%for extra in extras:
<input type="checkbox" id="${extra}" name="${extra}" value="1" ${extras[extra]} />${string.capwords(extra)}<br>
%endfor
<br>
<input id="submit" type="submit" value="Fetch Extras">
</form>
</div>
<div id="dialog" title="Choose Which Extras to Fetch" style="display:none" class="configtable">
<form action="getExtras" method="get" class="form">
<input type="hidden" name="ArtistID" value="${artist['ArtistID']}">
<input type="hidden" name="newstyle" value="true">
%for extra in extras:
<input type="checkbox" id="${extra}" name="${extra}" value="1" ${extras[extra]} />${string.capwords(extra)}<br>
%endfor
<br>
<input id="submit" type="submit" value="Fetch Extras">
</form>
</div>
</div>
</div>
<a href="home" class="back">&laquo; Back to overview</a>
@@ -42,14 +42,8 @@
<div id="artistImg">
<img id="artistImage" class="albumArt" alt="" src="artwork/artist/${artist['ArtistID']}"/>
</div>
<h1>
%if artist['Status'] == 'Loading':
<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 id="artistname">
<a href="http://musicbrainz.org/artist/${artist['ArtistID']}" id="artistnamelink">${artist['ArtistName']}</a>
</h1>
<div id="artistBio"></div>
</div>
@@ -62,6 +56,7 @@
<option value="Wanted">Wanted</option>
<option value="WantedNew">Wanted (new only)</option>
<option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option>
</select>
<input type="hidden" value="Go">
@@ -89,6 +84,8 @@
grade = 'X'
elif album['Status'] == 'Snatched':
grade = 'C'
elif album['Status'] == 'Ignored':
grade = 'I'
else:
grade = 'A'
@@ -123,15 +120,15 @@
%>
<tr class="grade${grade}">
<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="reldate">${album['ReleaseDate']}</td>
<td id="type">${album['Type']}</td>
<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>]
%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:
[<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
@@ -153,9 +150,6 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
%if artist['Status'] == 'Loading':
<meta http-equiv="refresh" content="5">
%endif
</%def>
<%def name="javascriptIncludes()">
@@ -168,23 +162,22 @@
getInfo(elem,id,'artist');
}
<%
if headphones.SONGKICK_FILTER_ENABLED:
if headphones.CONFIG.SONGKICK_FILTER_ENABLED:
songkick_filter_enabled = "true"
else:
songkick_filter_enabled = "false"
if not headphones.SONGKICK_LOCATION:
if not headphones.CONFIG.SONGKICK_LOCATION:
songkick_location = "none"
else:
songkick_location = headphones.SONGKICK_LOCATION
if headphones.SONGKICK_ENABLED:
songkick_location = headphones.CONFIG.SONGKICK_LOCATION
if headphones.CONFIG.SONGKICK_ENABLED:
songkick_enabled = "true"
else:
songkick_enabled = "false"
%>
function getArtistsCalendar() {
var template, calendarDomNode;
@@ -193,16 +186,16 @@
template = '<li><a target="_blank" href="URI"><span class="sk-name">NAME</span><span class="sk-location">LOC</span></a></li>';
$.getJSON("https://api.songkick.com/api/3.0/artists/mbid:${artist['ArtistID']}/calendar.json?apikey=${headphones.SONGKICK_APIKEY}&jsoncallback=?",
$.getJSON("https://api.songkick.com/api/3.0/artists/mbid:${artist['ArtistID']}/calendar.json?apikey=${headphones.CONFIG.SONGKICK_APIKEY}&jsoncallback=?",
function(data){
if (data['resultsPage'].totalEntries >= 1) {
if( ${songkick_filter_enabled} ) {
data.resultsPage.results.event = $.grep(data.resultsPage.results.event, function(element,index){
return element.venue.metroArea.id == ${songkick_location};
});
}
if (data['resultsPage'].totalEntries >= 1) {
if( ${songkick_filter_enabled} ) {
data.resultsPage.results.event = $.grep(data.resultsPage.results.event, function(element,index){
return element.venue.metroArea.id == ${songkick_location};
});
}
var tourDate;
calendarDomNode.show();
@@ -218,7 +211,7 @@
});
calendarDomNode.append('<li><img src="interfaces/default/images/songkick.png" alt="concerts by songkick" class="sk-logo" /></li>');
$(function() {
$("#artistCalendar").each(function() {
$("li:gt(4)", this).hide(); /* :gt() is zero-indexed */
@@ -237,18 +230,49 @@
});
}
function initThisPage() {
$('#menu_link_getextra').click(function() {
$('#dialog').dialog();
return false;
});
$('#menu_link_modifyextra').click(function() {
$('#dialog').dialog();
});
var loadingMessage = false;
var spinner_active = false;
var loadingtext_active = false;
%if artist['Status'] == 'Loading':
showMsg("Getting artist information",true);
%endif
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() {
$('#menu_link_getextra').click(function(event) {
$('#dialog').dialog();
event.preventDefault();
});
$('#menu_link_modifyextra').click(function(event) {
$('#dialog').dialog();
event.preventDefault();
});
$('#album_table').dataTable({
"bDestroy": true,
"aoColumns": [
@@ -280,7 +304,7 @@
resetFilters("albums");
setTimeout(function(){
initFancybox();
},1500)
},1500);
}
$(document).ready(function() {
@@ -288,8 +312,12 @@
initThisPage();
getArtistBio();
if( ${songkick_enabled} ){
getArtistsCalendar();
}
getArtistsCalendar();
}
checkArtistStatus();
setInterval(function(){
checkArtistStatus();
}, 1500);
});
</script>
+41 -16
View File
@@ -3,7 +3,6 @@
from headphones import version
%>
<!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 8 ]> <html lang="en" class="no-js ie8"> <![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="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/jquery-ui.css">
<link rel="stylesheet" href="interfaces/default/css/font-awesome.min.css">
${next.headIncludes()}
<script src="js/libs/modernizr-1.7.min.js"></script>
<script src="js/libs/modernizr-2.8.3.min.js"></script>
</head>
<body>
<div id="container">
<div id="ajaxMsg"></div>
<div id="ajaxMsg" class="ajaxMsg"></div>
% if not headphones.CURRENT_VERSION:
<div id="updatebar">
You're running an unknown version of Headphones. <a href="update">Update</a> or
@@ -37,7 +38,7 @@
</div>
% elif headphones.CURRENT_VERSION != headphones.LATEST_VERSION and headphones.COMMITS_BEHIND > 0 and headphones.INSTALL_TYPE != 'win':
<div id="updatebar">
A <a href="https://github.com/${headphones.GIT_USER}/headphones/compare/${headphones.CURRENT_VERSION}...${headphones.LATEST_VERSION}"> newer version</a> is available. You're ${headphones.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or <a href="#" onclick="$('#updatebar').slideUp('slow');">Close</a>
A <a href="https://github.com/${headphones.CONFIG.GIT_USER}/headphones/compare/${headphones.CURRENT_VERSION}...${headphones.LATEST_VERSION}"> newer version</a> is available. You're ${headphones.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or <a href="#" onclick="$('#updatebar').slideUp('slow');">Close</a>
</div>
% endif
@@ -58,11 +59,11 @@
<form action="search" method="get">
<input type="text" value="" placeholder="Search" onfocus="if(this.value==this.defaultValue) this.value='';" name="name" />
<i class='fa fa-search mini-icon'></i>
<select name="type">
<select name="type" id="search_type">
<option value="artist">Artist</option>
<option value="album">Album</option>
</select>
<input type="submit" value="Add"/>
<input type="submit" value="Search"/>
</form>
</div>
@@ -77,6 +78,12 @@
</div>
<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">
<small>
<a href="shutdown"><i class="fa fa-power-off"></i> Shutdown</a> |
@@ -89,29 +96,47 @@
%if version.HEADPHONES_VERSION != 'master':
(${version.HEADPHONES_VERSION})
%endif
%if headphones.GIT_BRANCH != 'master':
(${headphones.GIT_BRANCH})
%if headphones.CONFIG.GIT_BRANCH != 'master':
(${headphones.CONFIG.GIT_BRANCH})
%endif
</div>
</footer>
<a href="#main" id="toTop"><i class="fa fa-angle-double-up"></i> <span>Back to top</span></a>
</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/common.js"></script>
${next.javascriptIncludes()}
<script src="js/plugins.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 -->
<!-- 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') || "artist";
$("#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>
</html>
<%def name="javascriptIncludes()"></%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;
@alt-font-face: "Trebuchet MS", Helvetica, Arial, sans-serif;
@base-font-size: 12px;
@@ -10,20 +10,20 @@
@msg-bg: #FFF6A9;
@msg-bg-success: #D3FFD7;
@msg-bg-error: #FFD3D3;
/* Mixins */
.rounded(@radius: 5px) {
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
}
.roundedTop(@radius: 5px) {
-moz-border-radius-topleft: @radius;
-moz-border-radius-topright: @radius;
-webkit-border-top-right-radius: @radius;
-webkit-border-top-left-radius: @radius;
border-top-left-radius: @radius;
border-top-right-radius: @radius;
/* Mixins */
.rounded(@radius: 5px) {
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
}
.roundedTop(@radius: 5px) {
-moz-border-radius-topleft: @radius;
-moz-border-radius-topright: @radius;
-webkit-border-top-right-radius: @radius;
-webkit-border-top-left-radius: @radius;
border-top-left-radius: @radius;
border-top-right-radius: @radius;
}
.roundedLeftTop(@radius: 5px) {
-moz-border-radius-topleft: @radius;
@@ -34,14 +34,14 @@
-moz-border-radius-topright: @radius;
-webkit-border-top-right-radius: @radius;
border-top-right-radius: @radius;
}
.roundedBottom(@radius: 5px) {
-moz-border-radius-bottomleft: @radius;
-moz-border-radius-bottomright: @radius;
-webkit-border-bottom-right-radius: @radius;
-webkit-border-bottom-left-radius: @radius;
border-bottom-left-radius: @radius;
border-bottom-right-radius: @radius;
}
.roundedBottom(@radius: 5px) {
-moz-border-radius-bottomleft: @radius;
-moz-border-radius-bottomright: @radius;
-webkit-border-bottom-right-radius: @radius;
-webkit-border-bottom-left-radius: @radius;
border-bottom-left-radius: @radius;
border-bottom-right-radius: @radius;
}
.roundedLeftBottom(@radius: 5px) {
-moz-border-radius-bottomleft: @radius;
@@ -52,7 +52,7 @@
-moz-border-radius-bottomright: @radius;
-webkit-border-bottom-right-radius: @radius;
border-bottom-right-radius: @radius;
}
}
.shadow(@shadow: 0 17px 11px -1px #ced8d9) {
-moz-box-shadow: @shadow;
-webkit-box-shadow: @shadow;
@@ -74,4 +74,4 @@
-o-opacity:@opacity_percent / 100 !important;
opacity:@opacity_percent / 100 !important;
}
@@ -224,6 +224,14 @@ table.display tr.even.gradeA {
background-color: #ddffdd;
}
table.display tr.odd.gradeI {
background-color: #bebebe;
}
table.display tr.even.gradeI {
background-color: #bebebe;
}
table.display tr.odd.gradeC {
background-color: #ebf5ff;
}
@@ -254,6 +262,13 @@ table.display tr.even.gradeU {
background-color: #eee;
}
table.display tr.odd.gradeW {
background-color: #ffffaa;
}
table.display tr.even.gradeW {
background-color: #ffffaa;
}
table.display tr.odd.gradeZ {
background-color: #FAFAFA;
@@ -269,6 +284,7 @@ table.display tr.gradeL #status {
}
table.display tr.gradeA td,
table.display tr.gradeC td,
table.display tr.gradeI td,
table.display tr.gradeX td,
table.display tr.gradeU td,
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%; }
+80 -10
View File
@@ -163,6 +163,14 @@ img.albumArt {
max-height: 300px;
position: relative;
}
img.albumArt-nostretch {
float: left;
min-height: 64px;
min-width: 64px;
max-width: 250px;
max-height: 300px;
position: relative;
}
.title {
margin-bottom: 20px;
margin-top: 10px;
@@ -328,6 +336,10 @@ form .row label {
padding-top: 7px;
width: 175px;
}
form .row label.inline {
margin-right: 5px;
width: auto;
}
form .row input {
margin-right: 5px;
}
@@ -495,7 +507,7 @@ header .wrapper {
margin: 0 auto;
overflow: hidden;
position: relative;
width: 960px;
width: 990px;
}
header #logo {
float: left;
@@ -537,7 +549,7 @@ footer {
position: relative;
top: 4px;
}
#ajaxMsg {
.ajaxMsg {
border: 1px solid #cccccc;
background-image: -moz-linear-gradient(#ffffff, #eeeeee) !important;
background-image: linear-gradient(#ffffff, #eeeeee) !important;
@@ -568,16 +580,16 @@ footer {
-o-opacity: 0.8 !important;
opacity: 0.8 !important;
}
#ajaxMsg .msg {
.ajaxMsg .msg {
font-family: "Trebuchet MS", Helvetica, Arial, sans-serif;
line-height: normal;
padding-left: 20px;
}
#ajaxMsg .loader {
.ajaxMsg .loader {
position: relative;
top: 2px;
}
#ajaxMsg.success {
.ajaxMsg .success {
background-image: -moz-linear-gradient(#d3ffd7, #c2edc6) !important;
background-image: linear-gradient(#d3ffd7, #c2edc6) !important;
background-image: -webkit-linear-gradient(#d3ffd7, #c2edc6) !important;
@@ -587,7 +599,7 @@ footer {
padding: 15px 10px;
text-align: left;
}
#ajaxMsg.error {
.ajaxMsg .error {
background-image: -moz-linear-gradient(#ffd3d3, #edc4c4) !important;
background-image: linear-gradient(#ffd3d3, #edc4c4) !important;
background-image: -webkit-linear-gradient(#ffd3d3, #edc4c4) !important;
@@ -597,7 +609,7 @@ footer {
padding: 15px 10px;
text-align: left;
}
#ajaxMsg .ui-icon {
.ajaxMsg .ui-icon {
display: inline-block;
margin-left: -20px;
top: 2px;
@@ -750,7 +762,7 @@ div#searchbar input[type=text] {
line-height: normal;
margin-right: 5px;
padding: 4px 5px 4px 25px;
width: 150px;
width: 185px;
}
div#searchbar .mini-icon {
color: #999;
@@ -1118,13 +1130,34 @@ div#artistheader h2 a {
padding: 2px 10px;
}
#searchresults_table th#albumname {
min-width: 225px;
min-width: 250px;
text-align: left;
font-size: 14px;
}
#searchresults_table th#artistname {
min-width: 325px;
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 {
background: url("../images/loader_black.gif") no-repeat scroll center center #ffffff;
border: 3px solid #FFFFFF;
@@ -1136,15 +1169,40 @@ div#artistheader h2 a {
width: 50px;
}
#searchresults_table td#albumname {
min-width: 200px;
min-width: 250px;
text-align: left;
vertical-align: middle;
font-size: 14px;
}
#searchresults_table td#artistname {
min-width: 300px;
text-align: left;
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 {
vertical-align: middle;
}
@@ -1207,6 +1265,13 @@ div#artistheader h2 a {
float: left;
text-align: left;
}
#info {
float: left;
text-align: left;
margin-left: 10px;
margin-top: -5px;
color: #cccccc;
}
#actions {
float: right;
text-align: right;
@@ -1408,6 +1473,11 @@ div#artistheader h3 span {
min-width: 75px;
text-align: center;
}
#searchresults_table th#scoresmall {
min-width: 50px;
text-align: center;
font-size: 14px;
}
#track_table td#bitrate,
#track_table td#format {
font-size: 12px;
+12
View File
@@ -191,6 +191,11 @@ form {
line-height: normal;
padding-top: 7px;
width: 175px;
&.inline {
margin-right: 5px;
width: auto;
}
}
input { margin-right: 5px; }
input[type=text], input[type=password] {
@@ -938,6 +943,13 @@ div#artistheader {
float: left;
text-align: left;
}
#info {
float: left;
text-align: left;
margin-left: 10px;
margin-top: -5px;
color: @border-color;
}
#actions {
float: right;
text-align: right;
+9 -5
View File
@@ -1,6 +1,7 @@
<%inherit file="base.html"/>
<%!
from headphones import helpers
import cgi
%>
<%def name="headerIncludes()">
@@ -51,11 +52,11 @@
%>
<tr class="grade${grade}">
<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="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="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="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=${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>
%endfor
</tbody>
@@ -80,8 +81,11 @@
"sInfoFiltered":"(filtered from _MAX_ total items)"},
"iDisplayLength": 25,
"sPaginationType": "full_numbers",
"aaSorting": []
"aaSorting": [],
"fnDrawCallback": function (o) {
// Jump to top of page
$('html,body').scrollTop(0);
}
});
resetFilters("history");
}
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

+121 -118
View File
@@ -10,7 +10,7 @@
<th id="albumart"></th>
<th id="name">Artist Name</th>
<th id="status">Status</th>
<th id="album">Latest Album</th>
<th id="album">Latest Release</th>
<th id="have">Have</th>
</tr>
</thead>
@@ -24,129 +24,132 @@
</%def>
<%def name="javascriptIncludes()">
<script src="js/libs/jquery.unveil.min.js"></script>
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
function initThisPage() {
$('#artist_table').dataTable({
"bDestroy": true,
"aoColumnDefs": [
{
"bSortable": false,
"aTargets": [ 0 ],
"mData":"ArtistID",
"mRender": function ( data, type, full ) {
return '<div id="artistImg"><img class="albumArt" alt="" id="'+ 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>'
function initThisPage() {
$('#artist_table').dataTable({
"bDestroy": true,
"aoColumnDefs": [
{
"bSortable": false,
"aTargets": [0],
"mData":"ArtistID",
"mRender": function ( data, type, full ) {
return '<div id="artistImg"><img class="albumArt" alt="" id="'+ data + '" data-src="artwork/thumbs/artist/' + data + '"/></div>';
}
},
{"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,
"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) } )
{
"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": " ",
},
"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)
{
},
"fnDrawCallback": function (o) {
// Jump to top of page
$('html,body').scrollTop(0);
}
});
$('#artist_table').on("draw.dt", function () {
$("img").unveil();
});
resetFilters("artist or album");
}
resetFilters("artist or album");
}
$(document).ready(function() {
initThisPage();
});
$(window).load(function(){
initFancybox();
refreshLoadArtist();
});
$(document).ready(function() {
initThisPage();
});
$(window).load(function(){
initFancybox();
refreshLoadArtist();
});
</script>
</%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' ) {
var infoURL = "getImageLinks?ArtistID=" + id;
} else {
var infoURL = "getImageLinks?AlbumID=" + id;
}
// Get Data from the cache by ID
// Get Data from the cache by ID
$.ajax({
url: infoURL,
cache: true,
dataType: "json",
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";
}
else {
var thumbnail = data.thumbnail;
}
if ( data.artwork == "" || data.artwork == undefined ) {
if (!data.artwork) {
var artwork = "interfaces/default/images/no-cover-artist.png";
}
else {
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
var header = $("#container header");
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) {
// Set Message
feedback = $("#ajaxMsg");
@@ -316,6 +343,10 @@ function doAjaxCall(url,elem,reload,form) {
}
if ( reload == "tabs") refreshTab();
if ( reload == "page") location.reload();
if ( reload == "submenu&table") {
refreshSubmenu();
refreshTable();
}
if ( form ) {
// Change the option to 'choose...'
$(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){
if ( $(".dataTables_filter").length > 0 ) {
$(".dataTables_filter input").attr("placeholder","filter " + text + "");
}
}
function preventDefault(){
$("a[href='#']").live('click', function(){
return false;
});
}
function initFancybox() {
if ( $("a[rel=dialog]").length > 0 ) {
$.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(){
init();
initHeader();
});
+53 -42
View File
@@ -1,10 +1,18 @@
lossless<%inherit file="base.html"/>
<%inherit file="base.html"/>
<%!
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()">
<div class="title">
<div id="paddingheader">
<h1 class="clearfix"><i class="fa fa-flag"></i> Logs</h1>
</div>
<table class="display" id="log_table">
@@ -19,7 +27,7 @@ lossless<%inherit file="base.html"/>
</tbody>
</table>
<br>
<div align="center">Refresh rate:
<div align="center">Refresh rate:
<select id="refreshrate" onchange="setRefresh()">
<option value="0" selected="selected">No Refresh</option>
<option value="5">5 Seconds</option>
@@ -29,7 +37,6 @@ lossless<%inherit file="base.html"/>
<option value="300">5 Minutes</option>
<option value="600">10 Minutes</option>
</select></div>
</%def>
<%def name="headIncludes()">
@@ -39,46 +46,50 @@ lossless<%inherit file="base.html"/>
<%def name="javascriptIncludes()">
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function() {
$('#log_table').dataTable( {
"bProcessing": true,
"bServerSide": true,
"sAjaxSource": 'getLog',
"sPaginationType": "full_numbers",
"aaSorting": [[0, 'desc']],
"iDisplayLength": 25,
"bStateSave": true,
"oLanguage": {
"sSearch":"",
"sLengthMenu":"Show _MENU_ lines per page",
"sEmptyTable": "No log information available",
"sInfo":"Showing _START_ to _END_ of _TOTAL_ lines",
"sInfoEmpty":"Showing 0 to 0 of 0 lines",
"sInfoFiltered":"(filtered from _MAX_ total lines)"},
"fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
if (aData[1] === "WARNING" || aData[1] === "ERROR")
{
$('td', nRow).closest('tr').addClass("gradeX");
}
else
{
$('td', nRow).closest('tr').addClass("gradeZ");
}
return nRow;
},
"fnServerData": function ( sSource, aoData, fnCallback ) {
/* Add some extra data to the sender */
$.getJSON( sSource, aoData, function (json) {
fnCallback(json)
} );
}
} );
} );
$(document).ready(function() {
initActions();
$('#log_table').dataTable( {
"bProcessing": true,
"bServerSide": true,
"sAjaxSource": 'getLog',
"sPaginationType": "full_numbers",
"aaSorting": [[0, 'desc']],
"iDisplayLength": 25,
"bStateSave": true,
"oLanguage": {
"sSearch":"Filter:",
"sLengthMenu":"Show _MENU_ lines per page",
"sEmptyTable": "No log information available",
"sInfo":"Showing _START_ to _END_ of _TOTAL_ lines",
"sInfoEmpty":"Showing 0 to 0 of 0 lines",
"sInfoFiltered":"(filtered from _MAX_ total lines)"},
"fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
if (aData[1] === "ERROR") {
$('td', nRow).closest('tr').addClass("gradeX");
} else if (aData[1] === "WARNING") {
$('td', nRow).closest('tr').addClass("gradeW");
} else {
$('td', nRow).closest('tr').addClass("gradeZ");
}
return nRow;
},
"fnDrawCallback": function (o) {
// Jump to top of page
$('html,body').scrollTop(0);
},
"fnServerData": function ( sSource, aoData, fnCallback ) {
/* Add some extra data to the sender */
$.getJSON(sSource, aoData, function (json) {
fnCallback(json)
});
}
});
});
</script>
<script>
var timer;
var timer;
function setRefresh()
{
refreshrate = document.getElementById('refreshrate');
+8 -7
View File
@@ -14,12 +14,13 @@
<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=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>
<a href="manageAlbums">Manage All Albums</a>
</div>
</div>
<a class="menu_link_edit" href="manageArtists"><i class="fa fa-pencil"></i> Manage Artists</a>
%if not headphones.ADD_ARTISTS:
%if not headphones.CONFIG.AUTO_ADD_ARTISTS:
<a class="menu_link_edit" href="manageNew"><i class="fa fa-pencil"></i> Manage New Artists</a>
%endif
<a class="menu_link_edit" href="manageUnmatched"><i class="fa fa-pencil"></i> Manage Unmatched</a>
@@ -52,18 +53,18 @@
<br/>
<div class="row">
<label for="">Path to directory</label>
%if headphones.MUSIC_DIR:
<input type="text" value="${headphones.MUSIC_DIR}" name="path" size="70" />
%if headphones.CONFIG.MUSIC_DIR:
<input type="text" value="${headphones.CONFIG.MUSIC_DIR}" name="path" size="70" />
%else:
<input type="text" value="Enter a Music Directory to scan" onfocus="if
(this.value==this.defaultValue) this.value='';" name="path" size="70" />
%endif
</div>
<div class="row checkbox">
<input type="checkbox" name="libraryscan" id="libraryscan" value="1" ${checked(headphones.LIBRARYSCAN)}><label>Automatically scan library</label>
<input type="checkbox" name="libraryscan" id="libraryscan" value="1" ${checked(headphones.CONFIG.LIBRARYSCAN)}><label>Automatically scan library</label>
</div>
<div class="row checkbox">
<input type="checkbox" name="autoadd" id="autoadd" value="1" ${checked(headphones.ADD_ARTISTS)}><label>Auto-add new artists</label>
<input type="checkbox" name="autoadd" id="autoadd" value="1" ${checked(headphones.CONFIG.AUTO_ADD_ARTISTS)}><label>Auto-add new artists</label>
</div>
</fieldset>
@@ -82,8 +83,8 @@
<div class="row">
<label for="">Username</label>
<%
if headphones.LASTFM_USERNAME:
lastfmvalue = headphones.LASTFM_USERNAME
if headphones.CONFIG.LASTFM_USERNAME:
lastfmvalue = headphones.CONFIG.LASTFM_USERNAME
else:
lastfmvalue = ''
%>
+38 -32
View File
@@ -25,6 +25,7 @@
<option value="WantedNew">Wanted (new only)</option>
<option value="WantedLossless">Wanted (lossless)</option>
<option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option>
</select>
<input type="hidden" value="Go">
@@ -50,6 +51,8 @@
grade = 'Z'
elif album['Status'] == 'Wanted':
grade = 'X'
elif album['Status'] == 'Ignored':
grade = 'I'
elif album['Status'] == 'Snatched':
grade = 'C'
else:
@@ -109,40 +112,43 @@
<%def name="javascriptIncludes()">
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
function initThisPage() {
$('#album_table').dataTable({
"bDestroy": true,
"aoColumns": [
null,
null,
null,
null,
null,
null,
{ "sType": "title-numeric"},
null,
null
],
"aoColumnDefs": [
{ 'bSortable': false, 'aTargets': [ 0 ] }
],
"oLanguage": {
"sLengthMenu":"Show _MENU_ albums per page",
"sEmptyTable": "No album information available",
"sInfo":"Showing _TOTAL_ albums",
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
"sInfoFiltered":"(filtered from _MAX_ total albums)",
"sSearch": ""},
"bPaginate": false,
"aaSorting": [[5, 'desc']],
"fnDrawCallback": function (o) {
// Jump to top of page
$('html,body').scrollTop(0);
}
function initThisPage() {
$('#album_table').dataTable({
"bDestroy": true,
"aoColumns": [
null,
null,
null,
null,
null,
null,
{ "sType": "title-numeric"},
null,
null
],
"aoColumnDefs": [
{ 'bSortable': false, 'aTargets': [ 0 ] }
],
"oLanguage": {
"sLengthMenu":"Show _MENU_ albums per page",
"sEmptyTable": "No album information available",
"sInfo":"Showing _TOTAL_ albums",
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
"sInfoFiltered":"(filtered from _MAX_ total albums)",
"sSearch": ""},
"bPaginate": false,
"aaSorting": [[5, 'desc']]
});
resetFilters("albums");
}
$(document).ready(function() {
initThisPage();
});
resetFilters("albums");
}
$(document).ready(function() {
initThisPage();
});
</script>
</%def>
+25 -25
View File
@@ -86,31 +86,31 @@
<%def name="javascriptIncludes()">
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
function initThisPage() {
$('#artist_table').dataTable({
"bDestroy":true,
"aoColumns": [
null,
null,
{ "sType": "title-string"},
null,
{ "sType": "title-string"},
null
],
"oLanguage": {
"sSearch" : "",
"sEmptyTable": " "
},
"bStateSave": true,
"bPaginate": false
function initThisPage() {
$('#artist_table').dataTable({
"bDestroy": true,
"aoColumns": [
null,
null,
{ "sType": "title-string"},
null,
{ "sType": "title-string"},
null
],
"oLanguage": {
"sSearch" : "",
"sEmptyTable": " "
},
"bStateSave": true,
"bPaginate": false
});
resetFilters("artists");
}
$(document).ready(function() {
initThisPage();
});
$(window).load(function(){
initFancybox();
});
resetFilters("artists");
}
$(document).ready(function() {
initThisPage();
});
$(window).load(function(){
initFancybox();
});
</script>
</%def>
+19 -17
View File
@@ -85,24 +85,26 @@
<%def name="javascriptIncludes()">
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function()
{
$('#artist_table').dataTable(
{
"bStateSave": true,
"bPaginate": true,
"oLanguage": {
"sSearch": "",
"sLengthMenu":"Show _MENU_ albums per page",
"sInfo":"Showing _START_ to _END_ of _TOTAL_ albums",
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
"sInfoFiltered":"(filtered from _MAX_ total albums)",
"sEmptyTable": " ",
},
"sPaginationType": "full_numbers",
});
$(document).ready(function() {
$('#artist_table').dataTable({
"bStateSave": true,
"bPaginate": true,
"oLanguage": {
"sSearch": "",
"sLengthMenu":"Show _MENU_ albums per page",
"sInfo":"Showing _START_ to _END_ of _TOTAL_ albums",
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
"sInfoFiltered":"(filtered from _MAX_ total albums)",
"sEmptyTable": " ",
},
"sPaginationType": "full_numbers",
"fnDrawCallback": function (o) {
// Jump to top of page
$('html,body').scrollTop(0);
}
});
initActions();
initActions();
});
function restore_Artist(clicked_id) {
+11 -13
View File
@@ -6,7 +6,7 @@
<%def name="headerIncludes()">
<div id="subhead_container">
<div id="subhead_menu">
<a id="menu_link_scan" onclick="doAjaxCall('musicScan?path=${headphones.MUSIC_DIR}&redirect=manageNew',$(this))" data-success="Music library is getting scanned">Scan Music Library</a>
<a id="menu_link_scan" onclick="doAjaxCall('musicScan?path=${headphones.CONFIG.MUSIC_DIR}&redirect=manageNew',$(this))" data-success="Music library is getting scanned">Scan Music Library</a>
</div>
</div>
<a href="manage" class="back">&laquo; Back to manage overview</a>
@@ -53,18 +53,16 @@
<%def name="javascriptIncludes()">
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function()
{
$('#artist_table').dataTable(
{
"aaSorting": [[1, 'asc']],
"bStateSave": false,
"bPaginate": false,
"oLanguage": {
"sSearch" : ""},
});
initActions();
$(document).ready(function() {
$('#artist_table').dataTable({
"aaSorting": [[1, 'asc']],
"bStateSave": false,
"bPaginate": false,
"oLanguage": {
"sSearch" : ""},
});
initActions();
});
</script>
</%def>
+18 -16
View File
@@ -118,22 +118,24 @@
<%def name="javascriptIncludes()">
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function()
{
$('#artist_table').dataTable(
{
"bStateSave": true,
"bPaginate": true,
"oLanguage": {
"sSearch": "",
"sLengthMenu":"Show _MENU_ albums per page",
"sInfo":"Showing _START_ to _END_ of _TOTAL_ albums",
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
"sInfoFiltered":"(filtered from _MAX_ total albums)",
"sEmptyTable": " ",
},
"sPaginationType": "full_numbers",
});
$(document).ready(function() {
$('#artist_table').dataTable({
"bStateSave": true,
"bPaginate": true,
"oLanguage": {
"sSearch": "",
"sLengthMenu":"Show _MENU_ albums per page",
"sInfo":"Showing _START_ to _END_ of _TOTAL_ albums",
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
"sInfoFiltered":"(filtered from _MAX_ total albums)",
"sEmptyTable": " ",
},
"sPaginationType": "full_numbers",
"fnDrawCallback": function (o) {
// Jump to top of page
$('html,body').scrollTop(0);
}
});
initActions();
});
+89 -47
View File
@@ -11,36 +11,51 @@
<th id="albumart"></th>
%if type == 'album':
<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>
<th id="mbrelid" style="display:none;"</th>
%else:
<th id="artistname">Artist Name</th>
<th id="score">Score</th>
%endif
<th id="artistname">Artist Name</th>
<th id="score">Score</th>
<th id="add"></th>
<th id="mb"></th>
</tr>
</thead>
<tbody>
%if searchresults:
%for result in searchresults:
<%
if result['score'] == 100:
grade = 'A'
else:
grade = 'Z'
%>
%for result in searchresults:
<%
if result['score'] == 100:
grade = 'A'
else:
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}">
%if type == 'album':
<td id="albumart"><div id="artistImg"><img title="${result['albumid']}" class="albumArt" height="50" width="50"></div></td>
%else:
<td id="albumart" style=" text-align: center; vertical-align: middle;"><div id="artistImg"><img title="${result['albumid']}" class="albumArt" height="50" width="50" onerror="tryCCA(this, '${caa_group_url}')"></div></td>
%else:
<td id="albumart"><div id="artistImg"><img title="${result['id']}" class="albumArt" height="50" width="50"></div></td>
%endif
%if type == 'album':
<td id="albumname"><a href="${result['albumurl']}">${result['title']}</a></td>
%endif
<td id="artistname"><a href="${result['url']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
<td id="score"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></td>
%if type == 'album':
<td id="add"><a href="addReleaseById?rid=${result['albumid']}"><i class="fa fa-plus"></i> Add this album</a></td>
%else:
<td id="add"><a href="addArtist?artistid=${result['id']}"><i class="fa fa-plus"></i> Add this artist</a></td>
<td id="albumname"><a href="addReleaseById?rid=${result['albumid']}&rgid=${result['rgid']}" title="${albuminfo}">${result['title']}</a></td>
<td id="artistnamesmall"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
<td id="format">${result['formats']}</td>
<td id="tracks">${result['tracks']}</td>
<td id="reldate">${result['date']}</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>
<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="mbrelid" style="display:none;">${result['albumid']}</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
</tr>
%endfor
@@ -55,28 +70,34 @@
</%def>
<%def name="javascriptIncludes()">
<script src="js/libs/jquery.unveil.min.js"></script>
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
function getArt() {
$("table#searchresults_table tr td#albumart img").each(function(){
var id = $(this).attr('title');
var image = $(this);
if ( !image.hasClass('done') ) {
image.addClass('done');
getImageLinks(image,id,"${type}");
}
});
}
function initThisPage() {
getArt();
$('#searchresults_table').dataTable(
{
<script type="text/javascript">
function tryCCA(element, url) {
element.onerror = function() {
element.onerror = null;
element.src = "interfaces/default/images/no-cover-art.png";
};
element.src = url;
}
function getArt() {
$("table#searchresults_table tr td#albumart img").each(function(){
var id = $(this).attr('title');
var image = $(this);
if (!image.hasClass('done')) {
image.addClass('done');
getImageLinks(image, id, "${type}", true);
}
});
}
function initThisPage() {
$('#searchresults_table').dataTable({
"bDestroy": true,
"aoColumnDefs": [
{ 'bSortable': false, 'aTargets': [ 0,3 ] }
],
{ 'bSortable': false, 'aTargets': [ 0 ] }
],
"oLanguage": {
"sLengthMenu":"Show _MENU_ results per page",
"sEmptyTable": "No results",
@@ -86,15 +107,36 @@
"sSearch" : ""},
"iDisplayLength": 25,
"sPaginationType": "full_numbers",
"aaSorting": []
"aaSorting": [],
"fnDrawCallback": function (o) {
// Jump to top of page
$('html,body').scrollTop(0);
}
});
$('#searchresults_table').on("draw.dt", function () {
getArt();
$("img").unveil();
});
getArt();
resetFilters("album");
}
$(document).ready(function(){
initThisPage();
});
$(window).load(function(){
initFancybox();
});
}
$(document).ready(function(){
initFancybox();
initThisPage();
});
</script>
<script type="text/javascript">
<%!
# Abuse JSON module for escaping JavaScript
import json
%>
$(document).ready(function() {
// Search parameter
$("#searchbar input[name=name]").val(${name | json.dumps});
// Album or artist
$("#searchbar select[name=type]").val(${type | json.dumps});
});
</script>
</%def>
+24 -23
View File
@@ -5,21 +5,21 @@
<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>
</div>
</div>
</div>
</%def>
<%def name="body()">
<div class="title">
<div id="paddingheader">
<h1 class="clearfix"><i class="fa fa-heart"></i> Wanted Albums</h1>
</div>
<form action="markAlbums" method="get" id="markAlbums">
<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">
<option disabled="disabled" selected="selected">Choose...</option>
<option value="Skipped">Skipped</option>
<option value="Ignored">Ignored</option>
<option value="Downloaded">Downloaded</option>
</select>
<input type="hidden" value="Go">
@@ -40,7 +40,7 @@
%for album in wanted:
<tr class="gradeZ">
<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="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
<td id="reldate">${album['ReleaseDate']}</td>
@@ -49,7 +49,7 @@
%endfor
</tbody>
</table>
</form>
</div>
<div class="title">
@@ -70,7 +70,7 @@
<tbody>
%for album in upcoming:
<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="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
<td id="reldate">${album['ReleaseDate']}</td>
@@ -88,24 +88,25 @@
</%def>
<%def name="javascriptIncludes()">
<script src="js/libs/jquery.unveil.min.js"></script>
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
function initThisPage() {
$('#wanted_table').dataTable({
"oLanguage": {
"sEmptyTable": " "
},
"bDestroy":true,
"bFilter": false,
"bInfo": false,
"bPaginate": false
function initThisPage() {
$("img").unveil();
$('#wanted_table').dataTable({
"oLanguage": {
"sEmptyTable": " "
},
"bDestroy": true,
"bFilter": false,
"bInfo": false,
"bPaginate": false
});
resetFilters("artists");
initActions();
}
$(document).ready(function() {
initThisPage();
});
resetFilters("artists");
initActions();
}
$(document).ready(function() {
initThisPage();
});
</script>
</%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;
}
+188 -963
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -13,15 +13,18 @@
# 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 request, db
from headphones import request, db, logger
def getAlbumArt(albumid):
myDB = db.DBConnection()
asin = myDB.action('SELECT AlbumASIN from albums WHERE AlbumID=?', [albumid]).fetchone()[0]
asin = myDB.action(
'SELECT AlbumASIN from albums WHERE AlbumID=?', [albumid]).fetchone()[0]
if asin:
return 'http://ec1.images-amazon.com/images/P/%s.01.LZZZZZZZ.jpg' % asin
def getCachedArt(albumid):
from headphones import cache
+70 -55
View File
@@ -14,69 +14,84 @@
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
import headphones
from headphones import db, logger
from headphones import db, logger, cache
def switch(AlbumID, ReleaseID):
'''
"""
Takes the contents from allalbums & alltracks (based on ReleaseID) and switches them into
the albums & tracks table.
'''
"""
logger.debug('Switching allalbums and alltracks')
myDB = db.DBConnection()
oldalbumdata = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone()
newalbumdata = myDB.action('SELECT * from allalbums WHERE ReleaseID=?', [ReleaseID]).fetchone()
newtrackdata = myDB.action('SELECT * from alltracks WHERE ReleaseID=?', [ReleaseID]).fetchall()
oldalbumdata = myDB.action(
'SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone()
newalbumdata = myDB.action(
'SELECT * from allalbums WHERE ReleaseID=?', [ReleaseID]).fetchone()
newtrackdata = myDB.action(
'SELECT * from alltracks WHERE ReleaseID=?', [ReleaseID]).fetchall()
myDB.action('DELETE from tracks WHERE AlbumID=?', [AlbumID])
controlValueDict = {"AlbumID": AlbumID}
controlValueDict = {"AlbumID": AlbumID}
newValueDict = {"ArtistID": newalbumdata['ArtistID'],
"ArtistName": newalbumdata['ArtistName'],
"AlbumTitle": newalbumdata['AlbumTitle'],
"ReleaseID": newalbumdata['ReleaseID'],
"AlbumASIN": newalbumdata['AlbumASIN'],
"ReleaseDate": newalbumdata['ReleaseDate'],
"Type": newalbumdata['Type'],
"ReleaseCountry": newalbumdata['ReleaseCountry'],
"ReleaseFormat": newalbumdata['ReleaseFormat']
}
myDB.upsert("albums", newValueDict, controlValueDict)
for track in newtrackdata:
controlValueDict = {"TrackID": track['TrackID'],
"AlbumID": AlbumID}
newValueDict = {"ArtistID": track['ArtistID'],
"ArtistName": track['ArtistName'],
"AlbumTitle": track['AlbumTitle'],
"AlbumASIN": track['AlbumASIN'],
"ReleaseID": track['ReleaseID'],
"TrackTitle": track['TrackTitle'],
"TrackDuration": track['TrackDuration'],
"TrackNumber": track['TrackNumber'],
"CleanName": track['CleanName'],
"Location": track['Location'],
"Format": track['Format'],
"BitRate": track['BitRate']
newValueDict = {"ArtistID": newalbumdata['ArtistID'],
"ArtistName": newalbumdata['ArtistName'],
"AlbumTitle": newalbumdata['AlbumTitle'],
"ReleaseID": newalbumdata['ReleaseID'],
"AlbumASIN": newalbumdata['AlbumASIN'],
"ReleaseDate": newalbumdata['ReleaseDate'],
"Type": newalbumdata['Type'],
"ReleaseCountry": newalbumdata['ReleaseCountry'],
"ReleaseFormat": newalbumdata['ReleaseFormat']
}
myDB.upsert("tracks", newValueDict, controlValueDict)
# Mark albums as downloaded if they have at least 80% (by default, configurable) of the album
total_track_count = len(newtrackdata)
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)):
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID])
# Update have track counts on index
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [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']]))
controlValueDict = {"ArtistID": newalbumdata['ArtistID']}
newValueDict = { "TotalTracks": totaltracks,
"HaveTracks": havetracks}
myDB.upsert("albums", newValueDict, controlValueDict)
# Update cache
c = cache.Cache()
c.remove_from_cache(AlbumID=AlbumID)
c.get_artwork_from_cache(AlbumID=AlbumID)
for track in newtrackdata:
controlValueDict = {"TrackID": track['TrackID'],
"AlbumID": AlbumID}
newValueDict = {"ArtistID": track['ArtistID'],
"ArtistName": track['ArtistName'],
"AlbumTitle": track['AlbumTitle'],
"AlbumASIN": track['AlbumASIN'],
"ReleaseID": track['ReleaseID'],
"TrackTitle": track['TrackTitle'],
"TrackDuration": track['TrackDuration'],
"TrackNumber": track['TrackNumber'],
"CleanName": track['CleanName'],
"Location": track['Location'],
"Format": track['Format'],
"BitRate": track['BitRate']
}
myDB.upsert("tracks", newValueDict, controlValueDict)
# Mark albums as downloaded if they have at least 80% (by default,
# configurable) of the album
total_track_count = len(newtrackdata)
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.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
myDB.action(
'UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID])
# Update have track counts on index
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']]))
controlValueDict = {"ArtistID": newalbumdata['ArtistID']}
newValueDict = {"TotalTracks": totaltracks,
"HaveTracks": havetracks}
myDB.upsert("artists", newValueDict, controlValueDict)
+154 -140
View File
@@ -13,156 +13,165 @@
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
import headphones
from headphones import db, mb, importer, searcher, cache, postprocessor, versioncheck, logger
import lib.simplejson as simplejson
from xml.dom.minidom import Document
import copy
import headphones
import json
cmd_list = [ 'getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSimilar', 'getHistory', 'getLogs',
cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSimilar', 'getHistory', 'getLogs',
'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist',
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'getVersion', 'checkGithub',
'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo',
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'getVersion', 'checkGithub',
'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo',
'getArtistThumb', 'getAlbumThumb', 'choose_specific_download', 'download_specific_release']
class Api(object):
def __init__(self):
self.apikey = None
self.cmd = None
self.id = None
self.kwargs = None
self.data = None
self.callback = None
def checkParams(self,*args,**kwargs):
if not headphones.API_ENABLED:
def checkParams(self, *args, **kwargs):
if not headphones.CONFIG.API_ENABLED:
self.data = 'API not enabled'
return
if not headphones.API_KEY:
if not headphones.CONFIG.API_KEY:
self.data = 'API key not generated'
return
if len(headphones.API_KEY) != 32:
if len(headphones.CONFIG.API_KEY) != 32:
self.data = 'API key not generated correctly'
return
if 'apikey' not in kwargs:
self.data = 'Missing api key'
return
if kwargs['apikey'] != headphones.API_KEY:
if kwargs['apikey'] != headphones.CONFIG.API_KEY:
self.data = 'Incorrect API key'
return
else:
self.apikey = kwargs.pop('apikey')
if 'cmd' not in kwargs:
self.data = 'Missing parameter: cmd'
return
if kwargs['cmd'] not in cmd_list:
self.data = 'Unknown command: %s' % kwargs['cmd']
return
else:
self.cmd = kwargs.pop('cmd')
self.kwargs = kwargs
self.data = 'OK'
def fetchData(self):
if self.data == 'OK':
logger.info('Recieved API command: %s', self.cmd)
methodToCall = getattr(self, "_" + self.cmd)
result = methodToCall(**self.kwargs)
methodToCall(**self.kwargs)
if 'callback' not in self.kwargs:
if type(self.data) == type(''):
if isinstance(self.data, basestring):
return self.data
else:
return simplejson.dumps(self.data)
return json.dumps(self.data)
else:
self.callback = self.kwargs['callback']
self.data = simplejson.dumps(self.data)
self.data = json.dumps(self.data)
self.data = self.callback + '(' + self.data + ');'
return self.data
else:
return self.data
def _dic_from_query(self,query):
def _dic_from_query(self, query):
myDB = db.DBConnection()
rows = myDB.select(query)
rows_as_dic = []
for row in rows:
row_as_dic = dict(zip(row.keys(), row))
rows_as_dic.append(row_as_dic)
return rows_as_dic
def _getIndex(self, **kwargs):
self.data = self._dic_from_query('SELECT * from artists order by ArtistSortName COLLATE NOCASE')
return
self.data = self._dic_from_query(
'SELECT * from artists order by ArtistSortName COLLATE NOCASE')
return
def _getArtist(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['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')
description = self._dic_from_query('SELECT * from descriptions WHERE ArtistID="' + self.id + '"')
self.data = { 'artist': artist, 'albums': albums, 'description' : description }
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')
description = self._dic_from_query(
'SELECT * from descriptions WHERE ArtistID="' + self.id + '"')
self.data = {
'artist': artist, 'albums': albums, 'description': description}
return
def _getAlbum(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['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 + '"')
description = self._dic_from_query('SELECT * from descriptions WHERE ReleaseGroupID="' + self.id + '"')
self.data = { 'album' : album, 'tracks' : tracks, 'description' : description }
album = self._dic_from_query(
'SELECT * from albums 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 + '"')
self.data = {
'album': album, 'tracks': tracks, 'description': description}
return
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
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
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
def _getSimilar(self, **kwargs):
self.data = self._dic_from_query('SELECT * from lastfmcloud')
return
def _getLogs(self, **kwargs):
pass
def _findArtist(self, **kwargs):
if 'name' not in kwargs:
self.data = 'Missing parameter: name'
@@ -170,8 +179,8 @@ class Api(object):
if 'limit' in kwargs:
limit = kwargs['limit']
else:
limit=50
limit = 50
self.data = mb.findArtist(kwargs['name'], limit)
def _findAlbum(self, **kwargs):
@@ -181,217 +190,217 @@ class Api(object):
if 'limit' in kwargs:
limit = kwargs['limit']
else:
limit=50
limit = 50
self.data = mb.findRelease(kwargs['name'], limit)
def _addArtist(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
try:
importer.addArtisttoDB(self.id)
except Exception, e:
except Exception as e:
self.data = e
return
def _delArtist(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
myDB = db.DBConnection()
myDB.action('DELETE from artists WHERE ArtistID="' + self.id + '"')
myDB.action('DELETE from albums WHERE ArtistID="' + self.id + '"')
myDB.action('DELETE from tracks WHERE ArtistID="' + self.id + '"')
def _pauseArtist(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
myDB = db.DBConnection()
controlValueDict = {'ArtistID': self.id}
newValueDict = {'Status': 'Paused'}
myDB.upsert("artists", newValueDict, controlValueDict)
def _resumeArtist(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
myDB = db.DBConnection()
controlValueDict = {'ArtistID': self.id}
newValueDict = {'Status': 'Active'}
myDB.upsert("artists", newValueDict, controlValueDict)
def _refreshArtist(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
try:
importer.addArtisttoDB(self.id)
except Exception, e:
except Exception as e:
self.data = e
return
def _addAlbum(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
try:
importer.addReleaseById(self.id)
except Exception, e:
except Exception as e:
self.data = e
return
def _queueAlbum(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
if 'new' in kwargs:
new = kwargs['new']
else:
new = False
if 'lossless' in kwargs:
lossless = kwargs['lossless']
else:
lossless = False
myDB = db.DBConnection()
controlValueDict = {'AlbumID': self.id}
if lossless:
newValueDict = {'Status': 'Wanted Lossless'}
else:
else:
newValueDict = {'Status': 'Wanted'}
myDB.upsert("albums", newValueDict, controlValueDict)
searcher.searchforalbum(self.id, new)
searcher.searchforalbum(self.id, new)
def _unqueueAlbum(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
myDB = db.DBConnection()
controlValueDict = {'AlbumID': self.id}
newValueDict = {'Status': 'Skipped'}
myDB.upsert("albums", newValueDict, controlValueDict)
def _forceSearch(self, **kwargs):
searcher.searchforalbum()
def _forceProcess(self, **kwargs):
self.dir = None
if 'dir' in kwargs:
self.dir = kwargs['dir']
postprocessor.forcePostProcess(self.dir)
def _getVersion(self, **kwargs):
self.data = {
'git_path' : headphones.GIT_PATH,
'install_type' : headphones.INSTALL_TYPE,
'current_version' : headphones.CURRENT_VERSION,
'latest_version' : headphones.LATEST_VERSION,
'commits_behind' : headphones.COMMITS_BEHIND,
self.data = {
'git_path': headphones.CONFIG.GIT_PATH,
'install_type': headphones.INSTALL_TYPE,
'current_version': headphones.CURRENT_VERSION,
'latest_version': headphones.LATEST_VERSION,
'commits_behind': headphones.COMMITS_BEHIND,
}
def _checkGithub(self, **kwargs):
versioncheck.checkGithub()
self._getVersion()
def _shutdown(self, **kwargs):
headphones.SIGNAL = 'shutdown'
def _restart(self, **kwargs):
headphones.SIGNAL = 'restart'
def _update(self, **kwargs):
headphones.SIGNAL = 'update'
def _getArtistArt(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
self.data = cache.getArtwork(ArtistID=self.id)
def _getAlbumArt(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
self.data = cache.getArtwork(AlbumID=self.id)
def _getArtistInfo(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
self.data = cache.getInfo(ArtistID=self.id)
def _getAlbumInfo(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
self.data = cache.getInfo(AlbumID=self.id)
def _getArtistThumb(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
self.data = cache.getThumb(ArtistID=self.id)
def _getAlbumThumb(self, **kwargs):
if 'id' not in kwargs:
self.data = 'Missing parameter: id'
return
else:
self.id = kwargs['id']
self.data = cache.getThumb(AlbumID=self.id)
def _choose_specific_download(self, **kwargs):
@@ -402,18 +411,19 @@ class Api(object):
else:
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 = []
for result in results:
result_dict = {
'title':result[0],
'size':result[1],
'url':result[2],
'provider':result[3],
'kind':result[4]
'title': result[0],
'size': result[1],
'url': result[2],
'provider': result[3],
'kind': result[4]
}
results_as_dicts.append(result_dict)
@@ -421,7 +431,7 @@ class Api(object):
def _download_specific_release(self, **kwargs):
expected_kwargs =['id', 'title','size','url','provider','kind']
expected_kwargs = ['id', 'title', 'size', 'url', 'provider', 'kind']
for kwarg in expected_kwargs:
if kwarg not in kwargs:
@@ -438,20 +448,24 @@ class Api(object):
for kwarg in expected_kwargs:
del kwargs[kwarg]
# Handle situations where the torrent url contains arguments that are parsed
# Handle situations where the torrent url contains arguments that are
# parsed
if kwargs:
import urllib, urllib2
url = urllib2.quote(url, safe=":?/=&") + '&' + urllib.urlencode(kwargs)
import urllib
import urllib2
url = urllib2.quote(
url, safe=":?/=&") + '&' + urllib.urlencode(kwargs)
try:
result = [(title,int(size),url,provider,kind)]
result = [(title, int(size), url, provider, kind)]
except ValueError:
result = [(title,float(size),url,provider,kind)]
result = [(title, float(size), url, provider, kind)]
logger.info(u"Making sure we can download the chosen result")
(data, bestqual) = searcher.preprocess(result)
if data and bestqual:
myDB = db.DBConnection()
album = myDB.action('SELECT * from albums WHERE AlbumID=?', [id]).fetchone()
searcher.send_to_downloader(data, bestqual, album)
myDB = db.DBConnection()
album = myDB.action(
'SELECT * from albums WHERE AlbumID=?', [id]).fetchone()
searcher.send_to_downloader(data, bestqual, album)
+100 -67
View File
@@ -14,13 +14,12 @@
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
import os
import glob
import urllib
import headphones
from headphones import db, helpers, logger, lastfm, request
lastfm_apikey = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
class Cache(object):
"""
@@ -40,34 +39,31 @@ class Cache(object):
and for info it is <musicbrainzid>.<date>.txt
"""
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
path_to_art_cache = os.path.join(headphones.CONFIG.CACHE_DIR, 'artwork')
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 = []
def _findfilesstartingwith(self,pattern,folder):
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):
files = []
if os.path.exists(folder):
for fname in os.listdir(folder):
if fname.startswith(pattern):
files.append(os.path.join(folder,fname))
files.append(os.path.join(folder, fname))
return files
def _exists(self, type):
@@ -75,14 +71,14 @@ class Cache(object):
self.thumb_files = []
if type == 'artwork':
self.artwork_files = self._findfilesstartingwith(self.id,self.path_to_art_cache)
self.artwork_files = self._findfilesstartingwith(self.id, self.path_to_art_cache)
if self.artwork_files:
return True
else:
return False
elif type == 'thumb':
self.thumb_files = self._findfilesstartingwith("T_" + self.id,self.path_to_art_cache)
self.thumb_files = self._findfilesstartingwith("T_" + self.id, self.path_to_art_cache)
if self.thumb_files:
return True
else:
@@ -91,11 +87,10 @@ class Cache(object):
def _get_age(self, date):
# There's probably a better way to do this
split_date = date.split('-')
days_old = int(split_date[0])*365 + int(split_date[1])*30 + int(split_date[2])
days_old = int(split_date[0]) * 365 + int(split_date[1]) * 30 + int(split_date[2])
return days_old
def _is_current(self, filename=None, date=None):
if filename:
@@ -126,9 +121,9 @@ class Cache(object):
return thumb_url
def get_artwork_from_cache(self, ArtistID=None, AlbumID=None):
'''
"""
Pass a musicbrainz id to this function (either ArtistID or AlbumID)
'''
"""
self.query_type = 'artwork'
@@ -152,9 +147,9 @@ class Cache(object):
return None
def get_thumb_from_cache(self, ArtistID=None, AlbumID=None):
'''
"""
Pass a musicbrainz id to this function (either ArtistID or AlbumID)
'''
"""
self.query_type = 'thumb'
@@ -194,11 +189,11 @@ class Cache(object):
if not db_info or not db_info['LastUpdated'] or not self._is_current(date=db_info['LastUpdated']):
self._update_cache()
info_dict = { 'Summary' : self.info_summary, 'Content' : self.info_content }
info_dict = {'Summary': self.info_summary, 'Content': self.info_content}
return info_dict
else:
info_dict = { 'Summary' : db_info['Summary'], 'Content' : db_info['Content'] }
info_dict = {'Summary': db_info['Summary'], 'Content': db_info['Content']}
return info_dict
def get_image_links(self, ArtistID=None, AlbumID=None):
@@ -209,14 +204,14 @@ class Cache(object):
if ArtistID:
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:
return
try:
image_url = data['artist']['image'][-1]['#text']
except Exception:
except (KeyError, IndexError):
logger.debug('No artist image found')
image_url = None
@@ -227,23 +222,53 @@ class Cache(object):
else:
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:
return
try:
image_url = data['artist']['image'][-1]['#text']
except Exception:
logger.debug('No artist image found')
image_url = data['album']['image'][-1]['#text']
except (KeyError, IndexError):
logger.debug('No album image found on last.fm')
image_url = None
thumb_url = self._get_thumb_url(data)
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}
def remove_from_cache(self, ArtistID=None, AlbumID=None):
"""
Pass a musicbrainz id to this function (either ArtistID or AlbumID)
"""
if ArtistID:
self.id = ArtistID
self.id_type = 'artist'
else:
self.id = AlbumID
self.id_type = 'album'
self.query_type = 'artwork'
if self._exists('artwork'):
for artwork_file in self.artwork_files:
try:
os.remove(artwork_file)
except:
logger.warn('Error deleting file from the cache: %s', artwork_file)
self.query_type = 'thumb'
if self._exists('thumb'):
for thumb_file in self.thumb_files:
try:
os.remove(thumb_file)
except Exception:
logger.warn('Error deleting file from the cache: %s', thumb_file)
def _update_cache(self):
'''
@@ -252,26 +277,27 @@ class Cache(object):
myDB = db.DBConnection()
# Since lastfm uses release ids rather than release group ids for albums, we have to do a artist + album search for albums
# Exception is when adding albums manually, then we should use release id
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:
return
try:
self.info_summary = data['artist']['bio']['summary']
except Exception:
except KeyError:
logger.debug('No artist bio summary found')
self.info_summary = None
try:
self.info_content = data['artist']['bio']['content']
except Exception:
except KeyError:
logger.debug('No artist bio found')
self.info_content = None
try:
image_url = data['artist']['image'][-1]['#text']
except Exception:
except KeyError:
logger.debug('No artist image found')
image_url = None
@@ -281,25 +307,30 @@ class Cache(object):
else:
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)
dbalbum = myDB.action('SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?', [self.id]).fetchone()
if dbalbum['ReleaseID'] != self.id:
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'], api_key=LASTFM_API_KEY)
if not data:
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
else:
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
if not data:
return
try:
self.info_summary = data['album']['wiki']['summary']
except Exception:
except KeyError:
logger.debug('No album summary found')
self.info_summary = None
try:
self.info_content = data['album']['wiki']['content']
except Exception:
except KeyError:
logger.debug('No album infomation found')
self.info_content = None
try:
image_url = data['album']['image'][-1]['#text']
except Exception:
except KeyError:
logger.debug('No album image link found')
image_url = None
@@ -310,13 +341,13 @@ class Cache(object):
#Save the content & summary to the database no matter what if we've opened up the url
if self.id_type == 'artist':
controlValueDict = {"ArtistID": self.id}
controlValueDict = {"ArtistID": self.id}
else:
controlValueDict = {"ReleaseGroupID": self.id}
controlValueDict = {"ReleaseGroupID": self.id}
newValueDict = {"Summary": self.info_summary,
"Content": self.info_content,
"LastUpdated": helpers.today()}
newValueDict = {"Summary": self.info_summary,
"Content": self.info_content,
"LastUpdated": helpers.today()}
myDB.upsert("descriptions", newValueDict, controlValueDict)
@@ -343,7 +374,7 @@ class Cache(object):
if not os.path.isdir(self.path_to_art_cache):
try:
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)
self.artwork_errors = True
self.artwork_url = image_url
@@ -359,16 +390,15 @@ class Cache(object):
artwork_path = os.path.join(self.path_to_art_cache, self.id + '.' + helpers.today() + ext)
try:
f = open(artwork_path, 'wb')
f.write(artwork)
f.close()
except Exception, e:
with open(artwork_path, 'wb') as f:
f.write(artwork)
except IOError as e:
logger.error('Unable to write to the cache dir: %s', e)
self.artwork_errors = True
self.artwork_url = image_url
# Grab the thumbnail as well if we're getting the full artwork (as long as it's missing/outdated
if thumb_url and self.query_type in ['thumb','artwork'] and not (self.thumb_files and self._is_current(self.thumb_files[0])):
if thumb_url and self.query_type in ['thumb', 'artwork'] and not (self.thumb_files and self._is_current(self.thumb_files[0])):
artwork = request.request_content(thumb_url, timeout=20)
if artwork:
@@ -376,7 +406,7 @@ class Cache(object):
if not os.path.isdir(self.path_to_art_cache):
try:
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)
self.thumb_errors = True
self.thumb_url = thumb_url
@@ -385,21 +415,21 @@ class Cache(object):
for thumb_file in self.thumb_files:
try:
os.remove(thumb_file)
except:
except Exception as e:
logger.error('Error deleting file from the cache: %s', thumb_file)
ext = os.path.splitext(image_url)[1]
thumb_path = os.path.join(self.path_to_art_cache, 'T_' + self.id + '.' + helpers.today() + ext)
try:
f = open(thumb_path, 'wb')
f.write(artwork)
f.close()
except Exception, e:
with open(thumb_path, 'wb') as f:
f.write(artwork)
except IOError as e:
logger.error('Unable to write to the cache dir: %s', e)
self.thumb_errors = True
self.thumb_url = image_url
def getArtwork(ArtistID=None, AlbumID=None):
c = Cache()
@@ -414,6 +444,7 @@ def getArtwork(ArtistID=None, AlbumID=None):
artwork_file = os.path.basename(artwork_path)
return "cache/artwork/" + artwork_file
def getThumb(ArtistID=None, AlbumID=None):
c = Cache()
@@ -428,6 +459,7 @@ def getThumb(ArtistID=None, AlbumID=None):
thumbnail_file = os.path.basename(artwork_path)
return "cache/artwork/" + thumbnail_file
def getInfo(ArtistID=None, AlbumID=None):
c = Cache()
@@ -436,6 +468,7 @@ def getInfo(ArtistID=None, AlbumID=None):
return info_dict
def getImageLinks(ArtistID=None, AlbumID=None):
c = Cache()
+136 -130
View File
@@ -1,130 +1,136 @@
# 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/>.
#########################################
## Stolen from Sick-Beard's classes.py ##
#########################################
import headphones
import urllib
import datetime
from common import USER_AGENT
class HeadphonesURLopener(urllib.FancyURLopener):
version = USER_AGENT
class AuthURLOpener(HeadphonesURLopener):
"""
URLOpener class that supports http auth without needing interactive password entry.
If the provided username/password don't work it simply fails.
user: username to use for HTTP auth
pw: password to use for HTTP auth
"""
def __init__(self, user, pw):
self.username = user
self.password = pw
# remember if we've tried the username/password before
self.numTries = 0
# call the base class
urllib.FancyURLopener.__init__(self)
def prompt_user_passwd(self, host, realm):
"""
Override this function and instead of prompting just give the
username/password that were provided when the class was instantiated.
"""
# if this is the first try then provide a username/password
if self.numTries == 0:
self.numTries = 1
return (self.username, self.password)
# if we've tried before then return blank which cancels the request
else:
return ('', '')
# this is pretty much just a hack for convenience
def openit(self, url):
self.numTries = 0
return HeadphonesURLopener.open(self, url)
class SearchResult:
"""
Represents a search result from an indexer.
"""
def __init__(self):
self.provider = -1
# URL to the NZB/torrent file
self.url = ""
# used by some providers to store extra info associated with the result
self.extraInfo = []
# quality of the release
self.quality = -1
# release name
self.name = ""
def __str__(self):
if self.provider == None:
return "Invalid provider, unable to print self"
myString = self.provider.name + " @ " + self.url + "\n"
myString += "Extra Info:\n"
for extra in self.extraInfo:
myString += " " + extra + "\n"
return myString
class NZBSearchResult(SearchResult):
"""
Regular NZB result with an URL to the NZB
"""
resultType = "nzb"
class NZBDataSearchResult(SearchResult):
"""
NZB result where the actual NZB XML data is stored in the extraInfo
"""
resultType = "nzbdata"
class TorrentSearchResult(SearchResult):
"""
Torrent result with an URL to the torrent
"""
resultType = "torrent"
class Proper:
def __init__(self, name, url, date):
self.name = name
self.url = url
self.date = date
self.provider = None
self.quality = -1
self.tvdbid = -1
self.season = -1
self.episode = -1
def __str__(self):
return str(self.date)+" "+self.name+" "+str(self.season)+"x"+str(self.episode)+" of "+str(self.tvdbid)
# 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/>.
#########################################
## Stolen from Sick-Beard's classes.py ##
#########################################
import urllib
from common import USER_AGENT
class HeadphonesURLopener(urllib.FancyURLopener):
version = USER_AGENT
class AuthURLOpener(HeadphonesURLopener):
"""
URLOpener class that supports http auth without needing interactive password entry.
If the provided username/password don't work it simply fails.
user: username to use for HTTP auth
pw: password to use for HTTP auth
"""
def __init__(self, user, pw):
self.username = user
self.password = pw
# remember if we've tried the username/password before
self.numTries = 0
# call the base class
urllib.FancyURLopener.__init__(self)
def prompt_user_passwd(self, host, realm):
"""
Override this function and instead of prompting just give the
username/password that were provided when the class was instantiated.
"""
# if this is the first try then provide a username/password
if self.numTries == 0:
self.numTries = 1
return (self.username, self.password)
# if we've tried before then return blank which cancels the request
else:
return ('', '')
# this is pretty much just a hack for convenience
def openit(self, url):
self.numTries = 0
return HeadphonesURLopener.open(self, url)
class SearchResult:
"""
Represents a search result from an indexer.
"""
def __init__(self):
self.provider = -1
# URL to the NZB/torrent file
self.url = ""
# used by some providers to store extra info associated with the result
self.extraInfo = []
# quality of the release
self.quality = -1
# release name
self.name = ""
def __str__(self):
if self.provider is None:
return "Invalid provider, unable to print self"
myString = self.provider.name + " @ " + self.url + "\n"
myString += "Extra Info:\n"
for extra in self.extraInfo:
myString += " " + extra + "\n"
return myString
class NZBSearchResult(SearchResult):
"""
Regular NZB result with an URL to the NZB
"""
resultType = "nzb"
class NZBDataSearchResult(SearchResult):
"""
NZB result where the actual NZB XML data is stored in the extraInfo
"""
resultType = "nzbdata"
class TorrentSearchResult(SearchResult):
"""
Torrent result with an URL to the torrent
"""
resultType = "torrent"
class Proper:
def __init__(self, name, url, date):
self.name = name
self.url = url
self.date = date
self.provider = None
self.quality = -1
self.tvdbid = -1
self.season = -1
self.episode = -1
def __str__(self):
return str(self.date) + " " + self.name + " " + str(self.season) + "x" + str(self.episode) + " of " + str(self.tvdbid)
+181 -177
View File
@@ -1,177 +1,181 @@
# 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/>.
'''
Created on Aug 1, 2011
@author: Michael
'''
import platform, operator, os, re
from headphones import version
#Identify Our Application
USER_AGENT = 'Headphones/-'+version.HEADPHONES_VERSION+' ('+platform.system()+' '+platform.release()+')'
### Notification Types
NOTIFY_SNATCH = 1
NOTIFY_DOWNLOAD = 2
notifyStrings = {}
notifyStrings[NOTIFY_SNATCH] = "Started Download"
notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished"
### Release statuses
UNKNOWN = -1 # should never happen
UNAIRED = 1 # releases that haven't dropped yet
SNATCHED = 2 # qualified with quality
WANTED = 3 # releases we don't have but want to get
DOWNLOADED = 4 # qualified with quality
SKIPPED = 5 # releases we don't want
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
SNATCHED_PROPER = 9 # qualified with quality
class Quality:
NONE = 0
B192 = 1<<1 # 2
VBR = 1<<2 # 4
B256 = 1<<3 # 8
B320 = 1<<4 #16
FLAC = 1<<5 #32
# put these bits at the other end of the spectrum, far enough out that they shouldn't interfere
UNKNOWN = 1<<15
qualityStrings = {NONE: "N/A",
UNKNOWN: "Unknown",
B192: "MP3 192",
VBR: "MP3 VBR",
B256: "MP3 256",
B320: "MP3 320",
FLAC: "Flac"}
statusPrefixes = {DOWNLOADED: "Downloaded",
SNATCHED: "Snatched"}
@staticmethod
def _getStatusStrings(status):
toReturn = {}
for x in Quality.qualityStrings.keys():
toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status]+" ("+Quality.qualityStrings[x]+")"
return toReturn
@staticmethod
def combineQualities(anyQualities, bestQualities):
anyQuality = 0
bestQuality = 0
if anyQualities:
anyQuality = reduce(operator.or_, anyQualities)
if bestQualities:
bestQuality = reduce(operator.or_, bestQualities)
return anyQuality | (bestQuality<<16)
@staticmethod
def splitQuality(quality):
anyQualities = []
bestQualities = []
for curQual in Quality.qualityStrings.keys():
if curQual & quality:
anyQualities.append(curQual)
if curQual<<16 & quality:
bestQualities.append(curQual)
return (anyQualities, bestQualities)
@staticmethod
def nameQuality(name):
name = os.path.basename(name)
# if we have our exact text then assume we put it there
for x in Quality.qualityStrings:
if x == Quality.UNKNOWN:
continue
regex = '\W'+Quality.qualityStrings[x].replace(' ','\W')+'\W'
regex_match = re.search(regex, name, re.I)
if regex_match:
return x
checkName = lambda list, func: func([re.search(x, name, re.I) for x in list])
#TODO: fix quality checking here
if checkName(["mp3", "192"], any) and not checkName(["flac"], all):
return Quality.B192
elif checkName(["mp3", "256"], any) and not checkName(["flac"], all):
return Quality.B256
elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all):
return Quality.VBR
elif checkName(["mp3", "320"], any) and not checkName(["flac"], all):
return Quality.B320
else:
return Quality.UNKNOWN
@staticmethod
def assumeQuality(name):
if name.lower().endswith(".mp3"):
return Quality.MP3
elif name.lower().endswith(".flac"):
return Quality.LOSSLESS
else:
return Quality.UNKNOWN
@staticmethod
def compositeStatus(status, quality):
return status + 100 * quality
@staticmethod
def qualityDownloaded(status):
return (status - DOWNLOADED) / 100
@staticmethod
def splitCompositeStatus(status):
"""Returns a tuple containing (status, quality)"""
for x in sorted(Quality.qualityStrings.keys(), reverse=True):
if status > x*100:
return (status-x*100, x)
return (Quality.NONE, status)
@staticmethod
def statusFromName(name, assume=True):
quality = Quality.nameQuality(name)
if assume and quality == Quality.UNKNOWN:
quality = Quality.assumeQuality(name)
return Quality.compositeStatus(DOWNLOADED, quality)
DOWNLOADED = None
SNATCHED = None
SNATCHED_PROPER = None
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_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()]
MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], [])
LOSSLESS = Quality.combineQualities([Quality.FLAC], [])
ANY = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], [])
qualityPresets = (MP3, LOSSLESS, ANY)
qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)",
LOSSLESS: "Lossless (flac)",
ANY: "Any"}
# 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/>.
'''
Created on Aug 1, 2011
@author: Michael
'''
import platform
import operator
import os
import re
from headphones import version
#Identify Our Application
USER_AGENT = 'Headphones/-' + version.HEADPHONES_VERSION + ' (' + platform.system() + ' ' + platform.release() + ')'
### Notification Types
NOTIFY_SNATCH = 1
NOTIFY_DOWNLOAD = 2
notifyStrings = {}
notifyStrings[NOTIFY_SNATCH] = "Started Download"
notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished"
### Release statuses
UNKNOWN = -1 # should never happen
UNAIRED = 1 # releases that haven't dropped yet
SNATCHED = 2 # qualified with quality
WANTED = 3 # releases we don't have but want to get
DOWNLOADED = 4 # qualified with quality
SKIPPED = 5 # releases we don't want
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
SNATCHED_PROPER = 9 # qualified with quality
class Quality:
NONE = 0
B192 = 1 << 1 # 2
VBR = 1 << 2 # 4
B256 = 1 << 3 # 8
B320 = 1 << 4 #16
FLAC = 1 << 5 #32
# put these bits at the other end of the spectrum, far enough out that they shouldn't interfere
UNKNOWN = 1 << 15
qualityStrings = {NONE: "N/A",
UNKNOWN: "Unknown",
B192: "MP3 192",
VBR: "MP3 VBR",
B256: "MP3 256",
B320: "MP3 320",
FLAC: "Flac"}
statusPrefixes = {DOWNLOADED: "Downloaded",
SNATCHED: "Snatched"}
@staticmethod
def _getStatusStrings(status):
toReturn = {}
for x in Quality.qualityStrings.keys():
toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status] + " (" + Quality.qualityStrings[x] + ")"
return toReturn
@staticmethod
def combineQualities(anyQualities, bestQualities):
anyQuality = 0
bestQuality = 0
if anyQualities:
anyQuality = reduce(operator.or_, anyQualities)
if bestQualities:
bestQuality = reduce(operator.or_, bestQualities)
return anyQuality | (bestQuality << 16)
@staticmethod
def splitQuality(quality):
anyQualities = []
bestQualities = []
for curQual in Quality.qualityStrings.keys():
if curQual & quality:
anyQualities.append(curQual)
if curQual << 16 & quality:
bestQualities.append(curQual)
return (anyQualities, bestQualities)
@staticmethod
def nameQuality(name):
name = os.path.basename(name)
# if we have our exact text then assume we put it there
for x in Quality.qualityStrings:
if x == Quality.UNKNOWN:
continue
regex = '\W' + Quality.qualityStrings[x].replace(' ', '\W') + '\W'
regex_match = re.search(regex, name, re.I)
if regex_match:
return x
checkName = lambda list, func: func([re.search(x, name, re.I) for x in list])
#TODO: fix quality checking here
if checkName(["mp3", "192"], any) and not checkName(["flac"], all):
return Quality.B192
elif checkName(["mp3", "256"], any) and not checkName(["flac"], all):
return Quality.B256
elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all):
return Quality.VBR
elif checkName(["mp3", "320"], any) and not checkName(["flac"], all):
return Quality.B320
else:
return Quality.UNKNOWN
@staticmethod
def assumeQuality(name):
if name.lower().endswith(".mp3"):
return Quality.MP3
elif name.lower().endswith(".flac"):
return Quality.LOSSLESS
else:
return Quality.UNKNOWN
@staticmethod
def compositeStatus(status, quality):
return status + 100 * quality
@staticmethod
def qualityDownloaded(status):
return (status - DOWNLOADED) / 100
@staticmethod
def splitCompositeStatus(status):
"""Returns a tuple containing (status, quality)"""
for x in sorted(Quality.qualityStrings.keys(), reverse=True):
if status > x * 100:
return (status - x * 100, x)
return (Quality.NONE, status)
@staticmethod
def statusFromName(name, assume=True):
quality = Quality.nameQuality(name)
if assume and quality == Quality.UNKNOWN:
quality = Quality.assumeQuality(name)
return Quality.compositeStatus(DOWNLOADED, quality)
DOWNLOADED = None
SNATCHED = None
SNATCHED_PROPER = None
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_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()]
MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], [])
LOSSLESS = Quality.combineQualities([Quality.FLAC], [])
ANY = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], [])
qualityPresets = (MP3, LOSSLESS, ANY)
qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)",
LOSSLESS: "Lossless (flac)",
ANY: "Any"}
+434
View File
@@ -0,0 +1,434 @@
import headphones.logger
import itertools
import os
import re
from configobj import ConfigObj
def bool_int(value):
"""
Casts a config value into a 0 or 1
"""
if isinstance(value, basestring):
if value.lower() in ('', '0', 'false', 'f', 'no', 'n', 'off'):
value = 0
return int(bool(value))
_CONFIG_DEFINITIONS = {
'ADD_ALBUM_ART': (int, 'General', 0),
'ADVANCEDENCODER': (str, 'General', ''),
'ALBUM_ART_FORMAT': (str, 'General', 'folder'),
# This is used in importer.py to determine how complete an album needs to
# be - to be considered "downloaded". Percentage from 0-100
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
'API_ENABLED': (int, 'General', 0),
'API_KEY': (str, 'General', ''),
'AUTOWANT_ALL': (int, 'General', 0),
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
'AUTOWANT_UPCOMING': (int, 'General', 1),
'AUTO_ADD_ARTISTS': (int, 'General', 1),
'BITRATE': (int, 'General', 192),
'BLACKHOLE': (int, 'General', 0),
'BLACKHOLE_DIR': (str, 'General', ''),
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
'BOXCAR_ONSNATCH': (int, 'Boxcar', 0),
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
'CACHE_DIR': (str, 'General', ''),
'CACHE_SIZEMB': (int, 'Advanced', 32),
'CHECK_GITHUB': (int, 'General', 1),
'CHECK_GITHUB_INTERVAL': (int, 'General', 360),
'CHECK_GITHUB_ON_STARTUP': (int, 'General', 1),
'CLEANUP_FILES': (int, 'General', 0),
'CONFIG_VERSION': (str, 'General', '0'),
'CORRECT_METADATA': (int, 'General', 0),
'CUE_SPLIT': (int, 'General', 1),
'CUSTOMHOST': (str, 'General', 'localhost'),
'CUSTOMPORT': (int, 'General', 5000),
'CUSTOMSLEEP': (int, 'General', 1),
'DELETE_LOSSLESS_FILES': (int, 'General', 1),
'DESTINATION_DIR': (str, 'General', ''),
'DETECT_BITRATE': (int, 'General', 0),
'DOWNLOAD_DIR': (str, 'General', ''),
'DOWNLOAD_SCAN_INTERVAL': (int, 'General', 5),
'DOWNLOAD_TORRENT_DIR': (str, 'General', ''),
'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0),
'EMBED_ALBUM_ART': (int, 'General', 0),
'EMBED_LYRICS': (int, 'General', 0),
'ENABLE_HTTPS': (int, 'General', 0),
'ENCODER': (str, 'General', 'ffmpeg'),
'ENCODERFOLDER': (str, 'General', ''),
'ENCODERLOSSLESS': (int, 'General', 1),
'ENCODEROUTPUTFORMAT': (str, 'General', 'mp3'),
'ENCODERQUALITY': (int, 'General', 2),
'ENCODERVBRCBR': (str, 'General', 'cbr'),
'ENCODER_MULTICORE': (int, 'General', 0),
'ENCODER_MULTICORE_COUNT': (int, 'General', 0),
'ENCODER_PATH': (str, 'General', ''),
'EXTRAS': (str, 'General', ''),
'EXTRA_NEWZNABS': (list, 'Newznab', ''),
'FILE_FORMAT': (str, 'General', 'Track Artist - Album [Year] - Title'),
'FILE_PERMISSIONS': (str, 'General', '0644'),
'FILE_UNDERSCORES': (int, 'General', 0),
'FOLDER_FORMAT': (str, 'General', 'Artist/Album [Year]'),
'FOLDER_PERMISSIONS': (str, 'General', '0755'),
'FREEZE_DB': (int, 'General', 0),
'GIT_BRANCH': (str, 'General', 'master'),
'GIT_PATH': (str, 'General', ''),
'GIT_USER': (str, 'General', 'rembo10'),
'GROWL_ENABLED': (int, 'Growl', 0),
'GROWL_HOST': (str, 'Growl', ''),
'GROWL_ONSNATCH': (int, 'Growl', 0),
'GROWL_PASSWORD': (str, 'Growl', ''),
'HEADPHONES_INDEXER': (bool_int, 'General', False),
'HPPASS': (str, 'General', ''),
'HPUSER': (str, 'General', ''),
'HTTPS_CERT': (str, 'General', ''),
'HTTPS_KEY': (str, 'General', ''),
'HTTP_HOST': (str, 'General', '0.0.0.0'),
'HTTP_PASSWORD': (str, 'General', ''),
'HTTP_PORT': (int, 'General', 8181),
'HTTP_PROXY': (int, 'General', 0),
'HTTP_ROOT': (str, 'General', '/'),
'HTTP_USERNAME': (str, 'General', ''),
'IGNORED_WORDS': (str, 'General', ''),
'INCLUDE_EXTRAS': (int, 'General', 0),
'INTERFACE': (str, 'General', 'default'),
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
'KAT': (int, 'Kat', 0),
'KAT_PROXY_URL': (str, 'Kat', ''),
'KAT_RATIO': (str, 'Kat', ''),
'KEEP_NFO': (int, 'General', 0),
'KEEP_TORRENT_FILES': (int, 'General', 0),
'LASTFM_USERNAME': (str, 'General', ''),
'LAUNCH_BROWSER': (int, 'General', 1),
'LIBRARYSCAN': (int, 'General', 1),
'LIBRARYSCAN_INTERVAL': (int, 'General', 300),
'LMS_ENABLED': (int, 'LMS', 0),
'LMS_HOST': (str, 'LMS', ''),
'LOG_DIR': (str, 'General', ''),
'LOSSLESS_BITRATE_FROM': (int, 'General', 0),
'LOSSLESS_BITRATE_TO': (int, 'General', 0),
'LOSSLESS_DESTINATION_DIR': (str, 'General', ''),
'MB_IGNORE_AGE': (int, 'General', 365),
'MININOVA': (int, 'Mininova', 0),
'MININOVA_RATIO': (str, 'Mininova', ''),
'MIRROR': (str, 'General', 'musicbrainz.org'),
'MOVE_FILES': (int, 'General', 0),
'MPC_ENABLED': (bool_int, 'MPC', False),
'MUSIC_DIR': (str, 'General', ''),
'MUSIC_ENCODER': (int, 'General', 0),
'NEWZNAB': (int, 'Newznab', 0),
'NEWZNAB_APIKEY': (str, 'Newznab', ''),
'NEWZNAB_ENABLED': (int, 'Newznab', 1),
'NEWZNAB_HOST': (str, 'Newznab', ''),
'NMA_APIKEY': (str, 'NMA', ''),
'NMA_ENABLED': (int, 'NMA', 0),
'NMA_ONSNATCH': (int, 'NMA', 0),
'NMA_PRIORITY': (int, 'NMA', 0),
'NUMBEROFSEEDERS': (str, 'General', '10'),
'NZBGET_CATEGORY': (str, 'NZBget', ''),
'NZBGET_HOST': (str, 'NZBget', ''),
'NZBGET_PASSWORD': (str, 'NZBget', ''),
'NZBGET_PRIORITY': (int, 'NZBget', 0),
'NZBGET_USERNAME': (str, 'NZBget', 'nzbget'),
'NZBSORG': (int, 'NZBsorg', 0),
'NZBSORG_HASH': (str, 'NZBsorg', ''),
'NZBSORG_UID': (str, 'NZBsorg', ''),
'NZB_DOWNLOADER': (int, 'General', 0),
'OMGWTFNZBS': (int, 'omgwtfnzbs', 0),
'OMGWTFNZBS_APIKEY': (str, 'omgwtfnzbs', ''),
'OMGWTFNZBS_UID': (str, 'omgwtfnzbs', ''),
'OPEN_MAGNET_LINKS': (int, 'General', 0), # 0: Ignore, 1: Open, 2: Convert
'MAGNET_LINKS': (int, 'General', 0),
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/Headphones'),
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ONSNATCH': (int, 'OSX_Notify', 0),
'PIRATEBAY': (int, 'Piratebay', 0),
'PIRATEBAY_PROXY_URL': (str, 'Piratebay', ''),
'PIRATEBAY_RATIO': (str, 'Piratebay', ''),
'PLEX_CLIENT_HOST': (str, 'Plex', ''),
'PLEX_ENABLED': (int, 'Plex', 0),
'PLEX_NOTIFY': (int, 'Plex', 0),
'PLEX_PASSWORD': (str, 'Plex', ''),
'PLEX_SERVER_HOST': (str, 'Plex', ''),
'PLEX_UPDATE': (int, 'Plex', 0),
'PLEX_USERNAME': (str, 'Plex', ''),
'PREFERRED_BITRATE': (str, 'General', ''),
'PREFERRED_BITRATE_ALLOW_LOSSLESS': (int, 'General', 0),
'PREFERRED_BITRATE_HIGH_BUFFER': (int, 'General', 0),
'PREFERRED_BITRATE_LOW_BUFFER': (int, 'General', 0),
'PREFERRED_QUALITY': (int, 'General', 0),
'PREFERRED_WORDS': (str, 'General', ''),
'PREFER_TORRENTS': (int, 'General', 0),
'PROWL_ENABLED': (int, 'Prowl', 0),
'PROWL_KEYS': (str, 'Prowl', ''),
'PROWL_ONSNATCH': (int, 'Prowl', 0),
'PROWL_PRIORITY': (int, 'Prowl', 0),
'PUSHALOT_APIKEY': (str, 'Pushalot', ''),
'PUSHALOT_ENABLED': (int, 'Pushalot', 0),
'PUSHALOT_ONSNATCH': (int, 'Pushalot', 0),
'PUSHBULLET_APIKEY': (str, 'PushBullet', ''),
'PUSHBULLET_DEVICEID': (str, 'PushBullet', ''),
'PUSHBULLET_ENABLED': (int, 'PushBullet', 0),
'PUSHBULLET_ONSNATCH': (int, 'PushBullet', 0),
'PUSHOVER_APITOKEN': (str, 'Pushover', ''),
'PUSHOVER_ENABLED': (int, 'Pushover', 0),
'PUSHOVER_KEYS': (str, 'Pushover', ''),
'PUSHOVER_ONSNATCH': (int, 'Pushover', 0),
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
'RENAME_FILES': (int, 'General', 0),
'REPLACE_EXISTING_FOLDERS': (int, 'General', 0),
'REQUIRED_WORDS': (str, 'General', ''),
'RUTRACKER': (int, 'Rutracker', 0),
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
'RUTRACKER_RATIO': (str, 'Rutracker', ''),
'RUTRACKER_USER': (str, 'Rutracker', ''),
'SAB_APIKEY': (str, 'SABnzbd', ''),
'SAB_CATEGORY': (str, 'SABnzbd', ''),
'SAB_HOST': (str, 'SABnzbd', ''),
'SAB_PASSWORD': (str, 'SABnzbd', ''),
'SAB_USERNAME': (str, 'SABnzbd', ''),
'SAMPLINGFREQUENCY': (int, 'General', 44100),
'SEARCH_INTERVAL': (int, 'General', 1440),
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
'SONGKICK_ENABLED': (int, 'Songkick', 1),
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
'SONGKICK_LOCATION': (str, 'Songkick', ''),
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
'SUBSONIC_HOST': (str, 'Subsonic', ''),
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
'SUBSONIC_USERNAME': (str, 'Subsonic', ''),
'SYNOINDEX_ENABLED': (int, 'Synoindex', 0),
'TORRENTBLACKHOLE_DIR': (str, 'General', ''),
'TORRENT_DOWNLOADER': (int, 'General', 0),
'TORRENT_REMOVAL_INTERVAL': (int, 'General', 720),
'TRANSMISSION_HOST': (str, 'Transmission', ''),
'TRANSMISSION_PASSWORD': (str, 'Transmission', ''),
'TRANSMISSION_USERNAME': (str, 'Transmission', ''),
'TWITTER_ENABLED': (int, 'Twitter', 0),
'TWITTER_ONSNATCH': (int, 'Twitter', 0),
'TWITTER_PASSWORD': (str, 'Twitter', ''),
'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'),
'TWITTER_USERNAME': (str, 'Twitter', ''),
'UPDATE_DB_INTERVAL': (int, 'General', 24),
'USENET_RETENTION': (int, 'General', '1500'),
'UTORRENT_HOST': (str, 'uTorrent', ''),
'UTORRENT_LABEL': (str, 'uTorrent', ''),
'UTORRENT_PASSWORD': (str, 'uTorrent', ''),
'UTORRENT_USERNAME': (str, 'uTorrent', ''),
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
'WAFFLES': (int, 'Waffles', 0),
'WAFFLES_PASSKEY': (str, 'Waffles', ''),
'WAFFLES_RATIO': (str, 'Waffles', ''),
'WAFFLES_UID': (str, 'Waffles', ''),
'WHATCD': (int, 'What.cd', 0),
'WHATCD_PASSWORD': (str, 'What.cd', ''),
'WHATCD_RATIO': (str, 'What.cd', ''),
'WHATCD_USERNAME': (str, 'What.cd', ''),
'XBMC_ENABLED': (int, 'XBMC', 0),
'XBMC_HOST': (str, 'XBMC', ''),
'XBMC_NOTIFY': (int, 'XBMC', 0),
'XBMC_PASSWORD': (str, 'XBMC', ''),
'XBMC_UPDATE': (int, 'XBMC', 0),
'XBMC_USERNAME': (str, 'XBMC', ''),
'XLDPROFILE': (str, 'General', '')
}
# pylint:disable=R0902
# it might be nice to refactor for fewer instance variables
class Config(object):
""" Wraps access to particular values in a config file """
def __init__(self, config_file):
""" Initialize the config with values from a file """
self._config_file = config_file
self._config = ConfigObj(self._config_file, encoding='utf-8')
for key in _CONFIG_DEFINITIONS.keys():
self.check_setting(key)
self.ENCODER_MULTICORE_COUNT = max(0, self.ENCODER_MULTICORE_COUNT)
self._upgrade()
def _define(self, name):
key = name.upper()
ini_key = name.lower()
definition = _CONFIG_DEFINITIONS[key]
if len(definition) == 3:
definition_type, section, default = definition
else:
definition_type, section, _, default = definition
return key, definition_type, section, ini_key, default
def check_section(self, section):
""" Check if INI section exists, if not create it """
if section not in self._config:
self._config[section] = {}
return True
else:
return False
def check_setting(self, key):
""" Cast any value in the config to the right type or use the default """
key, definition_type, section, ini_key, default = self._define(key)
self.check_section(section)
try:
my_val = definition_type(self._config[section][ini_key])
except Exception:
my_val = definition_type(default)
self._config[section][ini_key] = my_val
return my_val
def write(self):
""" Make a copy of the stored config and write it to the configured file """
new_config = ConfigObj(encoding="UTF-8")
new_config.filename = self._config_file
# first copy over everything from the old config, even if it is not
# correctly defined to keep from losing data
for key, subkeys in self._config.items():
if key not in new_config:
new_config[key] = {}
for subkey, value in subkeys.items():
new_config[key][subkey] = value
# next make sure that everything we expect to have defined is so
for key in _CONFIG_DEFINITIONS.keys():
key, definition_type, section, ini_key, default = self._define(key)
self.check_setting(key)
if section not in new_config:
new_config[section] = {}
new_config[section][ini_key] = self._config[section][ini_key]
# Write it to file
headphones.logger.info("Writing configuration to file")
try:
new_config.write()
except IOError as e:
headphones.logger.error("Error writing configuration file: %s", e)
def get_extra_newznabs(self):
""" Return the extra newznab tuples """
extra_newznabs = list(
itertools.izip(*[itertools.islice(self.EXTRA_NEWZNABS, i, None, 3)
for i in range(3)])
)
return extra_newznabs
def clear_extra_newznabs(self):
""" Forget about the configured extra newznabs """
self.EXTRA_NEWZNABS = []
def add_extra_newznab(self, newznab):
""" Add a new extra newznab """
extra_newznabs = self.EXTRA_NEWZNABS
for item in newznab:
extra_newznabs.append(item)
self.EXTRA_NEWZNABS = extra_newznabs
def __getattr__(self, name):
"""
Returns something from the ini unless it is a real property
of the configuration object or is not all caps.
"""
if not re.match(r'[A-Z_]+$', name):
return super(Config, self).__getattr__(name)
else:
return self.check_setting(name)
def __setattr__(self, name, value):
"""
Maps all-caps properties to ini values unless they exist on the
configuration object.
"""
if not re.match(r'[A-Z_]+$', name):
super(Config, self).__setattr__(name, value)
return value
else:
key, definition_type, section, ini_key, default = self._define(name)
self._config[section][ini_key] = definition_type(value)
return self._config[section][ini_key]
def process_kwargs(self, kwargs):
"""
Given a big bunch of key value pairs, apply them to the ini.
"""
for name, value in kwargs.items():
key, definition_type, section, ini_key, default = self._define(name)
self._config[section][ini_key] = definition_type(value)
def _upgrade(self):
""" Update folder formats in the config & bump up config version """
if self.CONFIG_VERSION == '0':
from headphones.helpers import replace_all
file_values = {
'tracknumber': 'Track',
'title': 'Title',
'artist': 'Artist',
'album': 'Album',
'year': 'Year'
}
folder_values = {
'artist': 'Artist',
'album': 'Album',
'year': 'Year',
'releasetype': 'Type',
'first': 'First',
'lowerfirst': 'first'
}
self.FILE_FORMAT = replace_all(self.FILE_FORMAT, file_values)
self.FOLDER_FORMAT = replace_all(self.FOLDER_FORMAT, folder_values)
self.CONFIG_VERSION = '1'
if self.CONFIG_VERSION == '1':
from headphones.helpers import replace_all
file_values = {
'Track': '$Track',
'Title': '$Title',
'Artist': '$Artist',
'Album': '$Album',
'Year': '$Year',
'track': '$track',
'title': '$title',
'artist': '$artist',
'album': '$album',
'year': '$year'
}
folder_values = {
'Artist': '$Artist',
'Album': '$Album',
'Year': '$Year',
'Type': '$Type',
'First': '$First',
'artist': '$artist',
'album': '$album',
'year': '$year',
'type': '$type',
'first': '$first'
}
self.FILE_FORMAT = replace_all(self.FILE_FORMAT, file_values)
self.FOLDER_FORMAT = replace_all(self.FOLDER_FORMAT, folder_values)
self.CONFIG_VERSION = '2'
if self.CONFIG_VERSION == '2':
# Update the config to use direct path to the encoder rather than the encoder folder
if self.ENCODERFOLDER:
self.ENCODER_PATH = os.path.join(self.ENCODERFOLDER, self.ENCODER)
self.CONFIG_VERSION = '3'
if self.CONFIG_VERSION == '3':
# Update the BLACKHOLE option to the NZB_DOWNLOADER format
if self.BLACKHOLE:
self.NZB_DOWNLOADER = 2
self.CONFIG_VERSION = '4'
# Enable Headphones Indexer if they have a VIP account
if self.CONFIG_VERSION == '4':
if self.HPUSER and self.HPPASS:
self.HEADPHONES_INDEXER = True
self.CONFIG_VERSION = '5'
if self.CONFIG_VERSION == '5':
if self.OPEN_MAGNET_LINKS:
self.MAGNET_LINKS = 2
self.CONFIG_VERSION = '5'
+661
View File
@@ -0,0 +1,661 @@
# 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/>.
# Most of this lifted from here: https://github.com/SzieberthAdam/gneposis-cdgrab
import os
import sys
import re
import subprocess
import copy
import glob
import headphones
from headphones import logger
from mutagen.flac import FLAC
CUE_HEADER = {
'genre': '^REM GENRE (.+?)$',
'date': '^REM DATE (.+?)$',
'discid': '^REM DISCID (.+?)$',
'comment': '^REM COMMENT (.+?)$',
'catalog': '^CATALOG (.+?)$',
'artist': '^PERFORMER (.+?)$',
'title': '^TITLE (.+?)$',
'file': '^FILE (.+?) (WAVE|FLAC)$',
'accurateripid': '^REM ACCURATERIPID (.+?)$'
}
CUE_TRACK = 'TRACK (\d\d) AUDIO$'
CUE_TRACK_INFO = {
'artist': 'PERFORMER (.+?)$',
'title': 'TITLE (.+?)$',
'isrc': 'ISRC (.+?)$',
'index': 'INDEX (\d\d) (.+?)$'
}
ALBUM_META_FILE_NAME = 'album.dat'
SPLIT_FILE_NAME = 'split.dat'
ALBUM_META_ALBUM_BY_CUE = ('artist', 'title', 'date', 'genre')
HTOA_LENGTH_TRIGGER = 3
WAVE_FILE_TYPE_BY_EXTENSION = {
'.wav': 'Waveform Audio',
'.wv': 'WavPack',
'.ape': "Monkey's Audio",
'.m4a': 'Apple Lossless',
'.flac': 'Free Lossless Audio Codec'
}
# TODO: Only alow flac for now
#SHNTOOL_COMPATIBLE = ('Waveform Audio', 'WavPack', 'Free Lossless Audio Codec')
SHNTOOL_COMPATIBLE = ('Free Lossless Audio Codec')
# TODO: Make this better!
# this module-level variable is bad. :(
CUE_META = None
def check_splitter(command):
'''Check xld or shntools installed'''
try:
env = os.environ.copy()
if 'xld' in command:
env['PATH'] += os.pathsep + '/Applications'
devnull = open(os.devnull)
subprocess.Popen([command], stdout=devnull, stderr=devnull, env=env).communicate()
except OSError as e:
if e.errno == os.errno.ENOENT:
return False
return True
def split_baby(split_file, split_cmd):
'''Let's split baby'''
logger.info('Splitting %s...', split_file.decode(headphones.SYS_ENCODING, 'replace'))
logger.debug(subprocess.list2cmdline(split_cmd))
# Prevent Windows from opening a terminal window
startupinfo = None
if headphones.SYS_PLATFORM == "win32":
startupinfo = subprocess.STARTUPINFO()
try:
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
except AttributeError:
startupinfo.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW
env = os.environ.copy()
if 'xld' in split_cmd:
env['PATH'] += os.pathsep + '/Applications'
process = subprocess.Popen(split_cmd, startupinfo=startupinfo,
stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE,
stderr=subprocess.PIPE, env=env)
stdout, stderr = process.communicate()
if process.returncode:
logger.error('Split failed for %s', split_file.decode(headphones.SYS_ENCODING, 'replace'))
out = stdout if stdout else stderr
logger.error('Error details: %s', out.decode(headphones.SYS_ENCODING, 'replace'))
return False
else:
logger.info('Split success %s', split_file.decode(headphones.SYS_ENCODING, 'replace'))
return True
def check_list(list, ignore=0):
'''Checks a list for None elements. If list have None (after ignore index) then it should pass only if all elements
are None threreafter. Returns a tuple without the None entries.'''
if ignore:
try:
list[int(ignore)]
except:
raise ValueError('non-integer ignore index or ignore index not in list')
list1 = list[:ignore]
list2 = list[ignore:]
try:
first_none = list2.index(None)
except:
return tuple(list1 + list2)
for i in range(first_none, len(list2)):
if list2[i]:
raise ValueError('non-None entry after None entry in list at index {0}'.format(i))
while True:
list2.remove(None)
try:
list2.index(None)
except:
break
return tuple(list1 + list2)
def trim_cue_entry(string):
'''Removes leading and trailing "s.'''
if string[0] == '"' and string[-1] == '"':
string = string[1:-1]
return string
def int_to_str(value, length=2):
'''Converts integer to string eg 3 to "03"'''
try:
int(value)
except:
raise ValueError('expected an integer value')
content = str(value)
while len(content) < length:
content = '0' + content
return content
class Directory:
def __init__(self, path):
self.path = path
self.name = os.path.split(self.path)[-1]
self.content = []
self.update()
def filter(self, classname):
content = []
for c in self.content:
if c.__class__.__name__ == classname:
content.append(c)
return content
def tracks(self, ext=None, split=False):
content = []
for c in self.content:
ext_match = False
if c.__class__.__name__ == 'WaveFile':
if not ext or (ext and ext == c.name_ext):
ext_match = True
if ext_match and c.track_nr:
if not split or (split and c.split_file):
content.append(c)
return content
def update(self):
def check_match(filename):
for i in self.content:
if i.name == filename:
return True
return False
def identify_track_number(filename):
if 'split-track' in filename:
search = re.search('split-track(\d\d)', filename)
if search:
n = int(search.group(1))
if n:
return n
for n in range(0, 100):
search = re.search(int_to_str(n), filename)
if search:
# TODO: not part of other value such as year
return n
list_dir = glob.glob1(self.path, '*')
# TODO: for some reason removes only one file
rem_list = []
for i in self.content:
if i.name not in list_dir:
rem_list.append(i)
for i in rem_list:
self.content.remove(i)
for i in list_dir:
if not check_match(i):
# music file
if os.path.splitext(i)[-1] in WAVE_FILE_TYPE_BY_EXTENSION.keys():
track_nr = identify_track_number(i)
if track_nr:
self.content.append(WaveFile(self.path + os.sep + i, track_nr=track_nr))
else:
self.content.append(WaveFile(self.path + os.sep + i))
# cue file
elif os.path.splitext(i)[-1] == '.cue':
self.content.append(CueFile(self.path + os.sep + i))
# meta file
elif i == ALBUM_META_FILE_NAME:
self.content.append(MetaFile(self.path + os.sep + i))
# directory
elif os.path.isdir(i):
self.content.append(Directory(self.path + os.sep + i))
else:
self.content.append(File(self.path + os.sep + i))
class File(object):
def __init__(self, path):
self.path = path
self.name = os.path.split(self.path)[-1]
self.name_name = ''.join(os.path.splitext(self.name)[:-1])
self.name_ext = os.path.splitext(self.name)[-1]
self.split_file = True if self.name_name[:11] == 'split-track' else False
def get_name(self, ext=True, cmd=False):
if ext is True:
content = self.name
elif ext is False:
content = self.name_name
elif ext[0] == '.':
content = self.name_name + ext
else:
raise ValueError('ext parameter error')
if cmd:
content = content.replace(' ', '\ ')
return content
class CueFile(File):
def __init__(self, path):
def header_parser():
global line_content
c = self.content.splitlines()
header_dict = {}
#remaining_headers = CUE_HEADER
remaining_headers = copy.copy(CUE_HEADER)
line_index = 0
match = True
while match:
match = False
saved_match = None
line_content = c[line_index]
for e in remaining_headers:
search_result = re.search(remaining_headers[e], line_content, re.I)
if search_result:
search_content = trim_cue_entry(search_result.group(1))
header_dict[e] = search_content
saved_match = e
match = True
line_index += 1
if saved_match:
del remaining_headers[saved_match]
return header_dict, line_index
def track_parser(start_line):
c = self.content.splitlines()
line_index = start_line
line_content = c[line_index]
search_result = re.search(CUE_TRACK, line_content, re.I)
if not search_result:
raise ValueError('inconsistent CUE sheet, TRACK expected at line {0}'.format(line_index + 1))
track_nr = int(search_result.group(1))
line_index += 1
next_track = False
track_meta = {}
# we make room for future indexes
track_meta['index'] = [None for m in range(100)]
while not next_track:
if line_index < len(c):
line_content = c[line_index]
artist_search = re.search(CUE_TRACK_INFO['artist'], line_content, re.I)
title_search = re.search(CUE_TRACK_INFO['title'], line_content, re.I)
isrc_search = re.search(CUE_TRACK_INFO['isrc'], line_content, re.I)
index_search = re.search(CUE_TRACK_INFO['index'], line_content, re.I)
if artist_search:
if trim_cue_entry(artist_search.group(1)) != self.header['artist']:
track_meta['artist'] = trim_cue_entry(artist_search.group(1))
line_index += 1
elif title_search:
track_meta['title'] = trim_cue_entry(title_search.group(1))
line_index += 1
elif isrc_search:
track_meta['isrc'] = trim_cue_entry(isrc_search.group(1))
line_index += 1
elif index_search:
track_meta['index'][int(index_search.group(1))] = index_search.group(2)
line_index += 1
elif re.search(CUE_TRACK, line_content, re.I):
next_track = True
elif line_index == len(c) - 1 and not line_content:
# last line is empty
line_index += 1
elif re.search('FLAGS DCP$', line_content, re.I):
track_meta['dcpflag'] = True
line_index += 1
else:
raise ValueError('unknown entry in track error, line {0}'.format(line_index + 1))
else:
next_track = True
track_meta['index'] = check_list(track_meta['index'], ignore=1)
return track_nr, track_meta, line_index
super(CueFile, self).__init__(path)
try:
with open(self.name) as cue_file:
self.content = cue_file.read()
except:
self.content = None
if not self.content:
try:
with open(self.name, encoding="cp1252") as cue_file:
self.content = cue_file.read()
except:
raise ValueError('Cant encode CUE Sheet.')
if self.content[0] == u'\ufeff':
self.content = self.content[1:]
header = header_parser()
self.header = header[0]
line_index = header[1]
# we make room for tracks
tracks = [None for m in range(100)]
while line_index < len(self.content.splitlines()):
parsed_track = track_parser(line_index)
line_index = parsed_track[2]
tracks[parsed_track[0]] = parsed_track[1]
self.tracks = check_list(tracks, ignore=1)
def get_meta(self):
content = ''
for i in ALBUM_META_ALBUM_BY_CUE:
if self.header.get(i):
content += i + '\t' + self.header[i] + '\n'
else:
content += i + '\t' + '\n'
for i in range(len(self.tracks)):
if self.tracks[i]:
if self.tracks[i].get('artist'):
content += 'track' + int_to_str(i) + 'artist' + '\t' + self.tracks[i].get('artist') + '\n'
if self.tracks[i].get('title'):
content += 'track' + int_to_str(i) + 'title' + '\t' + self.tracks[i].get('title') + '\n'
return content
def htoa(self):
'''Returns true if Hidden Track exists.'''
if int(self.tracks[1]['index'][1][-5:-3]) >= HTOA_LENGTH_TRIGGER:
return True
return False
def breakpoints(self):
'''Returns track break points. Identical as CUETools' cuebreakpoints, with the exception of my standards for HTOA.'''
content = ''
for t in range(len(self.tracks)):
if t == 1 and not self.htoa():
content += ''
elif t >= 1:
t_index = self.tracks[t]['index']
content += t_index[1]
if (t < len(self.tracks) - 1):
content += '\n'
return content
class MetaFile(File):
def __init__(self, path):
super(MetaFile, self).__init__(path)
with open(self.path) as meta_file:
self.rawcontent = meta_file.read()
content = {}
content['tracks'] = [None for m in range(100)]
for l in self.rawcontent.splitlines():
parsed_line = re.search('^(.+?)\t(.+?)$', l)
if parsed_line:
if parsed_line.group(1)[:5] == 'track':
parsed_track = re.search('^track(\d\d)(.+?)$', parsed_line.group(1))
if not parsed_track:
raise ValueError('Syntax error in album meta file')
if not content['tracks'][int(parsed_track.group(1))]:
content['tracks'][int(parsed_track.group(1))] = dict()
content['tracks'][int(parsed_track.group(1))][parsed_track.group(2)] = parsed_line.group(2)
else:
content[parsed_line.group(1)] = parsed_line.group(2)
content['tracks'] = check_list(content['tracks'], ignore=1)
self.content = content
def flac_tags(self, track_nr):
common_tags = dict()
freeform_tags = dict()
# common flac tags
common_tags['artist'] = self.content['artist']
common_tags['album'] = self.content['title']
common_tags['title'] = self.content['tracks'][track_nr]['title']
common_tags['tracknumber'] = str(track_nr)
common_tags['tracktotal'] = str(len(self.content['tracks']) - 1)
if 'date' in self.content:
common_tags['date'] = self.content['date']
if 'genre' in CUE_META.content:
common_tags['genre'] = CUE_META.content['genre']
#freeform tags
#freeform_tags['country'] = self.content['country']
#freeform_tags['releasedate'] = self.content['releasedate']
return common_tags, freeform_tags
def folders(self):
artist = self.content['artist']
album = self.content['date'] + ' - ' + self.content['title'] + ' (' + self.content['label'] + ' - ' + self.content['catalog'] + ')'
return artist, album
def complete(self):
'''Check MetaFile for containing all data'''
self.__init__(self.path)
for l in self.rawcontent.splitlines():
if re.search('^[0-9A-Za-z]+?\t$', l):
return False
return True
def count_tracks(self):
'''Returns tracks count'''
return len(self.content['tracks']) - self.content['tracks'].count(None)
class WaveFile(File):
def __init__(self, path, track_nr=None):
super(WaveFile, self).__init__(path)
self.track_nr = track_nr
self.type = WAVE_FILE_TYPE_BY_EXTENSION[self.name_ext]
def filename(self, ext=None, cmd=False):
title = CUE_META.content['tracks'][self.track_nr]['title']
if ext:
if ext[0] != '.':
ext = '.' + ext
else:
ext = self.name_ext
f_name = int_to_str(self.track_nr) + ' - ' + title + ext
if cmd:
f_name = f_name.replace(' ', '\ ')
f_name = f_name.replace('!', '')
f_name = f_name.replace('?', '')
f_name = f_name.replace('/', ';')
return f_name
def tag(self):
if self.type == 'Free Lossless Audio Codec':
f = FLAC(self.name)
tags = CUE_META.flac_tags(self.track_nr)
for t in tags[0]:
f[t] = tags[0][t]
f.save()
def mutagen(self):
if self.type == 'Free Lossless Audio Codec':
return FLAC(self.name)
def split(albumpath):
global CUE_META
os.chdir(albumpath)
base_dir = Directory(os.getcwd())
cue = None
wave = None
# determining correct cue file
# if perfect match found
for _cue in base_dir.filter('CueFile'):
for _wave in base_dir.filter('WaveFile'):
if _cue.header['file'] == _wave.name:
logger.info('CUE Sheet found: {0}'.format(_cue.name))
logger.info('Music file found: {0}'.format(_wave.name))
cue = _cue
wave = _wave
# if no perfect match found then try without extensions
if not cue and not wave:
logger.info('No match for music files, trying to match without extensions...')
for _cue in base_dir.filter('CueFile'):
for _wave in base_dir.filter('WaveFile'):
if ''.join(os.path.splitext(_cue.header['file'])[:-1]) == _wave.name_name:
logger.info('Possible CUE Sheet found: {0}'.format(_cue.name))
logger.info('CUE Sheet refers music file: {0}'.format(_cue.header['file']))
logger.info('Possible Music file found: {0}'.format(_wave.name))
cue = _cue
wave = _wave
cue.header['file'] = wave.name
# if still no match then raise an exception
if not cue and not wave:
raise ValueError('No music file match found!')
# Split with xld or shntool
splitter = 'shntool'
xldprofile = None
# use xld profile to split cue
if headphones.CONFIG.ENCODER == 'xld' and headphones.CONFIG.MUSIC_ENCODER and headphones.CONFIG.XLDPROFILE:
import getXldProfile
xldprofile, xldformat, _ = getXldProfile.getXldProfile(headphones.CONFIG.XLDPROFILE)
if not xldformat:
raise ValueError('Details for xld profile "%s" not found, cannot split cue' % (xldprofile))
else:
if headphones.CONFIG.ENCODERFOLDER:
splitter = os.path.join(headphones.CONFIG.ENCODERFOLDER, 'xld')
else:
splitter = 'xld'
# use standard xld command to split cue
elif sys.platform == 'darwin':
splitter = 'xld'
if not check_splitter(splitter):
splitter = 'shntool'
if splitter == 'shntool' and not check_splitter(splitter):
raise ValueError('Command not found, ensure shntools with FLAC or xld (OS X) installed')
# Determine if file can be split (only flac allowed for shntools)
if 'xld' in splitter and wave.name_ext not in WAVE_FILE_TYPE_BY_EXTENSION.keys() or \
wave.type not in SHNTOOL_COMPATIBLE:
raise ValueError('Cannot split, audio file has unsupported extension')
# Split with xld
if 'xld' in splitter:
cmd = [splitter]
cmd.extend([wave.name])
cmd.extend(['-c'])
cmd.extend([cue.name])
if xldprofile:
cmd.extend(['--profile'])
cmd.extend([xldprofile])
else:
cmd.extend(['-f'])
cmd.extend(['flac'])
cmd.extend(['-o'])
cmd.extend([base_dir.path])
split = split_baby(wave.name, cmd)
else:
# Split with shntool
# generate temporary metafile describing the cue
with open(ALBUM_META_FILE_NAME, mode='w') as meta_file:
meta_file.write(cue.get_meta())
base_dir.content.append(MetaFile(os.path.abspath(ALBUM_META_FILE_NAME)))
# check metafile for completeness
if not base_dir.filter('MetaFile'):
raise ValueError('Cue Meta file {0} missing!'.format(ALBUM_META_FILE_NAME))
else:
CUE_META = base_dir.filter('MetaFile')[0]
with open(SPLIT_FILE_NAME, mode='w') as split_file:
split_file.write(cue.breakpoints())
cmd = ['shntool']
cmd.extend(['split'])
cmd.extend(['-f'])
cmd.extend([SPLIT_FILE_NAME])
cmd.extend(['-o'])
cmd.extend(['flac'])
cmd.extend([wave.name])
split = split_baby(wave.name, cmd)
os.remove(SPLIT_FILE_NAME)
base_dir.update()
# tag FLAC files
if split and CUE_META.count_tracks() == len(base_dir.tracks(ext='.flac', split=True)):
for t in base_dir.tracks(ext='.flac', split=True):
logger.info('Tagging {0}...'.format(t.name))
t.tag()
# rename FLAC files
if split and CUE_META.count_tracks() == len(base_dir.tracks(ext='.flac', split=True)):
for t in base_dir.tracks(ext='.flac', split=True):
if t.name != t.filename():
logger.info('Renaming {0} to {1}...'.format(t.name, t.filename()))
os.rename(t.name, t.filename())
os.remove(ALBUM_META_FILE_NAME)
if not split:
raise ValueError('Failed to split, check logs')
else:
# Rename original file
os.rename(wave.name, wave.name + '.original')
return True
+43 -47
View File
@@ -21,90 +21,86 @@ from __future__ import with_statement
import os
import sqlite3
import threading
import time
import headphones
from headphones import logger
def dbFilename(filename="headphones.db"):
return os.path.join(headphones.DATA_DIR, filename)
def getCacheSize():
#this will protect against typecasting problems produced by empty string and None settings
if not headphones.CACHE_SIZEMB:
if not headphones.CONFIG.CACHE_SIZEMB:
#sqlite will work with this (very slowly)
return 0
return int(headphones.CACHE_SIZEMB)
return int(headphones.CONFIG.CACHE_SIZEMB)
class DBConnection:
def __init__(self, filename="headphones.db"):
self.filename = filename
self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
#don't wait for the disk to finish writing
self.connection.execute("PRAGMA synchronous = OFF")
#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.CONFIG.JOURNAL_MODE)
#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
def action(self, query, args=None):
if query == None:
if query is None:
return
sqlResult = None
attempt = 0
while attempt < 5:
try:
if args == None:
#logger.debug(self.filename+": "+query)
sqlResult = self.connection.execute(query)
try:
with self.connection as c:
if args is None:
sqlResult = c.execute(query)
else:
#logger.debug(self.filename+": "+query+" with args "+str(args))
sqlResult = self.connection.execute(query, args)
self.connection.commit()
break
except sqlite3.OperationalError, e:
if "unable to open database file" in e.message or "database is locked" in e.message:
logger.warn('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)
sqlResult = c.execute(query, args)
except sqlite3.OperationalError, e:
if "unable to open database file" in e.message or "database is locked" in e.message:
logger.warn('Database Error: %s', e)
else:
logger.error('Database error: %s', e)
raise
except sqlite3.DatabaseError, e:
logger.error('Fatal Error executing %s :: %s', query, e)
raise
return sqlResult
def select(self, query, args=None):
sqlResults = self.action(query, args).fetchall()
if sqlResults == None:
if sqlResults is None or sqlResults == [None]:
return []
return sqlResults
def upsert(self, tableName, valueDict, keyDict):
changesBefore = self.connection.total_changes
genParams = lambda myDict : [x + " = ?" for x in myDict.keys()]
query = "UPDATE "+tableName+" SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
genParams = lambda myDict: [x + " = ?" for x in myDict.keys()]
query = "UPDATE " + tableName + " SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
self.action(query, valueDict.values() + keyDict.values())
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())) + ")"
self.action(query, valueDict.values() + keyDict.values())
+26 -41
View File
@@ -1,41 +1,26 @@
# 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/>.
def ex(e):
"""
Returns a string from the exception text if it exists.
"""
# sanity check
if not e.args or not e.args[0]:
return ""
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"
# 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/>.
class HeadphonesException(Exception):
"""
Generic Headphones Exception - should never be thrown, only subclassed
"""
class NewzbinAPIThrottled(HeadphonesException):
"""
Newzbin has throttled us, deal with it
"""
+9 -9
View File
@@ -2,26 +2,26 @@ import os.path
import plistlib
import sys
import xml.parsers.expat as expat
import commands
from headphones import logger
def getXldProfile(xldProfile):
xldProfileNotFound = xldProfile
expandedPath = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist')
try:
try:
preferences = plistlib.Plist.fromFile(expandedPath)
except (expat.ExpatError):
os.system("/usr/bin/plutil -convert xml1 %s" % expandedPath )
os.system("/usr/bin/plutil -convert xml1 %s" % expandedPath)
try:
preferences = plistlib.Plist.fromFile(expandedPath)
except (ImportError):
os.system("/usr/bin/plutil -convert binary1 %s" % expandedPath )
logger.info('The plist at "%s" has a date in it, and therefore is not useable.' % expandedPath)
os.system("/usr/bin/plutil -convert binary1 %s" % expandedPath)
logger.info('The plist at "%s" has a date in it, and therefore is not useable.', expandedPath)
return(xldProfileNotFound, None, None)
except (ImportError):
logger.info('The plist at "%s" has a date in it, and therefore is not useable.' % expandedPath)
logger.info('The plist at "%s" has a date in it, and therefore is not useable.', expandedPath)
except:
logger.info('Unexpected error:', sys.exc_info()[0])
logger.info('Unexpected error: %s', sys.exc_info()[0])
return(xldProfileNotFound, None, None)
xldProfile = xldProfile.lower()
@@ -61,7 +61,7 @@ def getXldProfile(xldProfile):
elif 'TVBR' in ShortDesc:
XLDAacOutput2_VBRQuality = int(profile.get('XLDAacOutput2_VBRQuality'))
if XLDAacOutput2_VBRQuality > 122:
xldBitrate = 320
xldBitrate = 320
elif XLDAacOutput2_VBRQuality > 113 and XLDAacOutput2_VBRQuality <= 122:
xldBitrate = 285
elif XLDAacOutput2_VBRQuality > 104 and XLDAacOutput2_VBRQuality <= 113:
@@ -178,4 +178,4 @@ def getXldProfile(xldProfile):
return(xldProfileForCmd, xldFormat, xldBitrate)
return(xldProfileNotFound, None, None)
return(xldProfileNotFound, None, None)
+184 -84
View File
@@ -19,6 +19,8 @@ import time
import shutil
import datetime
import headphones
import unicodedata
import sys
from operator import itemgetter
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
@@ -29,8 +31,9 @@ RE_FEATURING = re.compile(r"[fF]t\.|[fF]eaturing|[fF]eat\.|\b[wW]ith\b|&|vs\.")
RE_CD_ALBUM = re.compile(r"\(?((CD|disc)\s*[0-9]+)\)?", re.I)
RE_CD = re.compile(r"^(CD|dics)\s*[0-9]+$", re.I)
def multikeysort(items, columns):
comparers = [ ((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns]
comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns]
def comparer(left, right):
for fn, mult in comparers:
@@ -42,12 +45,14 @@ def multikeysort(items, columns):
return sorted(items, cmp=comparer)
def checked(variable):
if variable:
return 'Checked'
else:
return ''
def radio(variable, pos):
if variable == pos:
@@ -55,40 +60,41 @@ def radio(variable, pos):
else:
return ''
def latinToAscii(unicrap):
"""
From couch potato
"""
xlate = {0xc0:'A', 0xc1:'A', 0xc2:'A', 0xc3:'A', 0xc4:'A', 0xc5:'A',
0xc6:'Ae', 0xc7:'C',
0xc8:'E', 0xc9:'E', 0xca:'E', 0xcb:'E', 0x86:'e',
0xcc:'I', 0xcd:'I', 0xce:'I', 0xcf:'I',
0xd0:'Th', 0xd1:'N',
0xd2:'O', 0xd3:'O', 0xd4:'O', 0xd5:'O', 0xd6:'O', 0xd8:'O',
0xd9:'U', 0xda:'U', 0xdb:'U', 0xdc:'U',
0xdd:'Y', 0xde:'th', 0xdf:'ss',
0xe0:'a', 0xe1:'a', 0xe2:'a', 0xe3:'a', 0xe4:'a', 0xe5:'a',
0xe6:'ae', 0xe7:'c',
0xe8:'e', 0xe9:'e', 0xea:'e', 0xeb:'e', 0x0259:'e',
0xec:'i', 0xed:'i', 0xee:'i', 0xef:'i',
0xf0:'th', 0xf1:'n',
0xf2:'o', 0xf3:'o', 0xf4:'o', 0xf5:'o', 0xf6:'o', 0xf8:'o',
0xf9:'u', 0xfa:'u', 0xfb:'u', 0xfc:'u',
0xfd:'y', 0xfe:'th', 0xff:'y',
0xa1:'!', 0xa2:'{cent}', 0xa3:'{pound}', 0xa4:'{currency}',
0xa5:'{yen}', 0xa6:'|', 0xa7:'{section}', 0xa8:'{umlaut}',
0xa9:'{C}', 0xaa:'{^a}', 0xab:'<<', 0xac:'{not}',
0xad:'-', 0xae:'{R}', 0xaf:'_', 0xb0:'{degrees}',
0xb1:'{+/-}', 0xb2:'{^2}', 0xb3:'{^3}', 0xb4:"'",
0xb5:'{micro}', 0xb6:'{paragraph}', 0xb7:'*', 0xb8:'{cedilla}',
0xb9:'{^1}', 0xba:'{^o}', 0xbb:'>>',
0xbc:'{1/4}', 0xbd:'{1/2}', 0xbe:'{3/4}', 0xbf:'?',
0xd7:'*', 0xf7:'/'
xlate = {0xc0: 'A', 0xc1: 'A', 0xc2: 'A', 0xc3: 'A', 0xc4: 'A', 0xc5: 'A',
0xc6: 'Ae', 0xc7: 'C',
0xc8: 'E', 0xc9: 'E', 0xca: 'E', 0xcb: 'E', 0x86: 'e',
0xcc: 'I', 0xcd: 'I', 0xce: 'I', 0xcf: 'I',
0xd0: 'Th', 0xd1: 'N',
0xd2: 'O', 0xd3: 'O', 0xd4: 'O', 0xd5: 'O', 0xd6: 'O', 0xd8: 'O',
0xd9: 'U', 0xda: 'U', 0xdb: 'U', 0xdc: 'U',
0xdd: 'Y', 0xde: 'th', 0xdf: 'ss',
0xe0: 'a', 0xe1: 'a', 0xe2: 'a', 0xe3: 'a', 0xe4: 'a', 0xe5: 'a',
0xe6: 'ae', 0xe7: 'c',
0xe8: 'e', 0xe9: 'e', 0xea: 'e', 0xeb: 'e', 0x0259: 'e',
0xec: 'i', 0xed: 'i', 0xee: 'i', 0xef: 'i',
0xf0: 'th', 0xf1: 'n',
0xf2: 'o', 0xf3: 'o', 0xf4: 'o', 0xf5: 'o', 0xf6: 'o', 0xf8: 'o',
0xf9: 'u', 0xfa: 'u', 0xfb: 'u', 0xfc: 'u',
0xfd: 'y', 0xfe: 'th', 0xff: 'y',
0xa1: '!', 0xa2: '{cent}', 0xa3: '{pound}', 0xa4: '{currency}',
0xa5: '{yen}', 0xa6: '|', 0xa7: '{section}', 0xa8: '{umlaut}',
0xa9: '{C}', 0xaa: '{^a}', 0xab: '<<', 0xac: '{not}',
0xad: '-', 0xae: '{R}', 0xaf: '_', 0xb0: '{degrees}',
0xb1: '{+/-}', 0xb2: '{^2}', 0xb3: '{^3}', 0xb4: "'",
0xb5: '{micro}', 0xb6: '{paragraph}', 0xb7: '*', 0xb8: '{cedilla}',
0xb9: '{^1}', 0xba: '{^o}', 0xbb: '>>',
0xbc: '{1/4}', 0xbd: '{1/2}', 0xbe: '{3/4}', 0xbf: '?',
0xd7: '*', 0xf7: '/'
}
r = ''
for i in unicrap:
if xlate.has_key(ord(i)):
if ord(i) in xlate:
r += xlate[ord(i)]
elif ord(i) >= 0x80:
pass
@@ -96,9 +102,10 @@ def latinToAscii(unicrap):
r += str(i)
return r
def convert_milliseconds(ms):
seconds = ms/1000
seconds = ms / 1000
gmtime = time.gmtime(seconds)
if seconds > 3600:
minutes = time.strftime("%H:%M:%S", gmtime)
@@ -107,6 +114,7 @@ def convert_milliseconds(ms):
return minutes
def convert_seconds(s):
gmtime = time.gmtime(s)
@@ -117,15 +125,18 @@ def convert_seconds(s):
return minutes
def today():
today = datetime.date.today()
yyyymmdd = datetime.date.isoformat(today)
return yyyymmdd
def now():
now = datetime.datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S")
def get_age(date):
try:
@@ -134,22 +145,25 @@ def get_age(date):
return False
try:
days_old = int(split_date[0])*365 + int(split_date[1])*30 + int(split_date[2])
days_old = int(split_date[0]) * 365 + int(split_date[1]) * 30 + int(split_date[2])
except IndexError:
days_old = False
return days_old
def bytes_to_mb(bytes):
mb = int(bytes)/1048576
mb = int(bytes) / 1048576
size = '%.1f MB' % mb
return size
def mb_to_bytes(mb_str):
result = re.search('^(\d+(?:\.\d+)?)\s?(?:mb)?', mb_str, flags=re.I)
if result:
return int(float(result.group(1))*1048576)
return int(float(result.group(1)) * 1048576)
def piratesize(size):
split = size.split(" ")
@@ -168,15 +182,25 @@ def piratesize(size):
return size
def replace_all(text, dic):
def replace_all(text, dic, normalize=False):
if not text:
return ''
for i, j in dic.iteritems():
if normalize:
try:
if sys.platform == 'darwin':
j = unicodedata.normalize('NFD', j)
else:
j = unicodedata.normalize('NFC', j)
except TypeError:
j = unicodedata.normalize('NFC', j.decode(headphones.SYS_ENCODING, 'replace'))
text = text.replace(i, j)
return text
def replace_illegal_chars(string, type="file"):
if type == "file":
string = re.sub('[\?"*:|<>/]', '_', string)
@@ -185,6 +209,7 @@ def replace_illegal_chars(string, type="file"):
return string
def cleanName(string):
pass1 = latinToAscii(string).lower()
@@ -192,6 +217,7 @@ def cleanName(string):
return out_string
def cleanTitle(title):
title = re.sub('[\.\-\/\_]', ' ', title).lower()
@@ -203,6 +229,7 @@ def cleanTitle(title):
return title
def split_path(f):
"""
Split a path into components, starting with the drive letter (if any). Given
@@ -212,7 +239,7 @@ def split_path(f):
components = []
drive, path = os.path.splitdrive(f)
# Stip the folder from the path, iterate until nothing is left
# Strip the folder from the path, iterate until nothing is left
while True:
path, folder = os.path.split(path)
@@ -234,6 +261,7 @@ def split_path(f):
# Done
return components
def expand_subfolders(f):
"""
Try to expand a given folder and search for subfolders containing media
@@ -262,7 +290,7 @@ def expand_subfolders(f):
return
# Split into path components
media_folders = [ split_path(media_folder) for media_folder in media_folders ]
media_folders = [split_path(media_folder) for media_folder in media_folders]
# Correct folder endings such as CD1 etc.
for index, media_folder in enumerate(media_folders):
@@ -270,25 +298,25 @@ def expand_subfolders(f):
media_folders[index] = media_folders[index][:-1]
# Verify the result by computing path depth relative to root.
path_depths = [ len(media_folder) for media_folder in media_folders ]
path_depths = [len(media_folder) for media_folder in media_folders]
difference = max(path_depths) - min(path_depths)
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
# directory may contain separate CD's and maybe some extra's. The
# structure may look like X albums at same depth, and (one or more)
# extra folders with a higher depth.
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 = [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]))
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
# Convert back to paths and remove duplicates, which may be there after
# correcting the paths
media_folders = list(set([ os.path.join(*media_folder) for media_folder in media_folders ]))
media_folders = list(set([os.path.join(*media_folder) for media_folder in media_folders]))
# Don't return a result if the number of subfolders is one. In this case,
# this algorithm will not improve processing and will likely interfere
@@ -297,26 +325,18 @@ def expand_subfolders(f):
logger.debug("Did not expand subfolder, as it resulted in one folder.")
return
logger.debug("Expanded subfolders in folder: " % media_folders)
logger.debug("Expanded subfolders in folder: %s", media_folders)
return media_folders
def extract_data(s):
s = s.replace('_', ' ')
#headphones default format
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s\[(?P<year>.*?)\]', re.VERBOSE)
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s[\[\(](?P<year>.*?)[\]\)]', re.VERBOSE)
match = pattern.match(s)
if match:
name = match.group("name")
album = match.group("album")
year = match.group("year")
return (name, album, year)
#newzbin default format
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s\((?P<year>\d+?\))', re.VERBOSE)
match = pattern.match(s)
if match:
name = match.group("name")
album = match.group("album")
@@ -336,6 +356,7 @@ def extract_data(s):
else:
return (None, None, None)
def extract_metadata(f):
"""
Scan all files in the given directory and decide on an artist, album and
@@ -360,7 +381,7 @@ def extract_metadata(f):
# Try to read the file info
try:
media_file = MediaFile(os.path.join(root, file))
except FileTypeError, UnreadableFileError:
except (FileTypeError, UnreadableFileError):
# Probably not a media file
continue
@@ -374,20 +395,20 @@ def extract_metadata(f):
# Verify results
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)
# Require that some percentage of files have tags
count_ratio = 0.75
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)
# Count distinct values
artists = list(set([ x[0] for x in results ]))
albums = list(set([ x[1] for x in results ]))
years = list(set([ x[2] for x in results ]))
artists = list(set([x[0] for x in results]))
albums = list(set([x[1] for x in results]))
years = list(set([x[2] for x in results]))
# Remove things such as CD2 from album names
if len(albums) > 1:
@@ -399,7 +420,7 @@ def extract_metadata(f):
old_album = new_albums[index]
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
new_albums = list(set(new_albums))
@@ -415,9 +436,9 @@ def extract_metadata(f):
# (Lots of) different artists. Could be a featuring album, so test for this.
if len(artists) > 1 and len(albums) == 1:
split_artists = [ RE_FEATURING.split(artist) for artist in artists ]
featurings = [ len(split_artist) - 1 for split_artist in split_artists ]
logger.info("Album seem to feature %d different artists" % sum(featurings))
split_artists = [RE_FEATURING.split(x) for x in artists]
featurings = [len(split_artist) - 1 for split_artist in split_artists]
logger.info("Album seem to feature %d different artists", sum(featurings))
if sum(featurings) > 0:
# Find the artist of which the least splits have been generated.
@@ -429,11 +450,84 @@ def extract_metadata(f):
return (artist, albums[0], years[0])
# 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.debug("Artists: %s, Albums: %s, Years: %s" % (artists, albums, 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)
return (None, None, None)
def get_downloaded_track_list(albumpath):
"""
Return a list of audio files for the given directory.
"""
downloaded_track_list = []
for root, dirs, files in os.walk(albumpath):
for _file in files:
extension = os.path.splitext(_file)[1].lower()[1:]
if extension in headphones.MEDIA_FORMATS:
downloaded_track_list.append(os.path.join(root, _file))
return downloaded_track_list
def preserve_torrent_direcory(albumpath):
"""
Copy torrent directory to headphones-modified to keep files for seeding.
"""
from headphones import logger
new_folder = os.path.join(albumpath, 'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
logger.info("Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
try:
shutil.copytree(albumpath, new_folder)
return new_folder
except Exception as e:
logger.warn("Cannot copy/move files to temp folder: " + \
new_folder.decode(headphones.SYS_ENCODING, 'replace') + \
". Not continuing. Error: " + str(e))
return None
def cue_split(albumpath):
"""
Attempts to check and split audio files by a cue for the given directory.
"""
# Walk directory and scan all media files
count = 0
cue_count = 0
cue_dirs = []
for root, dirs, files in os.walk(albumpath):
for _file in files:
extension = os.path.splitext(_file)[1].lower()[1:]
if extension in headphones.MEDIA_FORMATS:
count += 1
elif extension == 'cue':
cue_count += 1
if root not in cue_dirs:
cue_dirs.append(root)
# Split cue
if cue_count and cue_count >= count and cue_dirs:
from headphones import logger, cuesplit
logger.info("Attempting to split audio files by cue")
cwd = os.getcwd()
for cue_dir in cue_dirs:
try:
cuesplit.split(cue_dir)
except Exception as e:
os.chdir(cwd)
logger.warn("Cue not split: " + str(e))
return False
os.chdir(cwd)
return True
return False
def extract_logline(s):
# Default log format
pattern = re.compile(r'(?P<timestamp>.*?)\s\-\s(?P<level>.*?)\s*\:\:\s(?P<thread>.*?)\s\:\s(?P<message>.*)', re.VERBOSE)
@@ -447,14 +541,11 @@ def extract_logline(s):
else:
return None
def extract_song_data(s):
from headphones import logger
#headphones default format
music_dir = headphones.MUSIC_DIR
folder_format = headphones.FOLDER_FORMAT
file_format = headphones.FILE_FORMAT
full_format = os.path.join(headphones.MUSIC_DIR)
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s\[(?P<year>.*?)\]', re.VERBOSE)
match = pattern.match(s)
@@ -478,6 +569,7 @@ def extract_song_data(s):
logger.info("Couldn't parse %s into a valid Newbin format", s)
return (name, album, year)
def smartMove(src, dest, delete=True):
from headphones import logger
@@ -486,7 +578,7 @@ def smartMove(src, dest, delete=True):
filename = os.path.basename(src)
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]
ext = os.path.splitext(filename)[1]
i = 1
@@ -499,7 +591,7 @@ def smartMove(src, dest, delete=True):
try:
os.rename(src, os.path.join(source_dir, newfile))
filename = newfile
except Exception, e:
except Exception as e:
logger.warn('Error renaming %s: %s', src.decode(headphones.SYS_ENCODING, 'replace'), e)
break
@@ -509,7 +601,7 @@ def smartMove(src, dest, delete=True):
else:
shutil.copy(os.path.join(source_dir, filename), os.path.join(dest, filename))
return True
except Exception, e:
except Exception as e:
logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'), e)
#########################
@@ -518,32 +610,36 @@ def smartMove(src, dest, delete=True):
# TODO: Grab config values from sab to know when these options are checked. For now we'll just iterate through all combinations
def sab_replace_dots(name):
return name.replace('.',' ')
return name.replace('.', ' ')
def sab_replace_spaces(name):
return name.replace(' ','_')
return name.replace(' ', '_')
def sab_sanitize_foldername(name):
""" Return foldername with dodgy chars converted to safe ones
Remove any leading and trailing dot and space characters
"""
CH_ILLEGAL = r'\/<>?*|"'
CH_LEGAL = r'++{}!@#`'
CH_LEGAL = r'++{}!@#`'
FL_ILLEGAL = CH_ILLEGAL + ':\x92"'
FL_LEGAL = CH_LEGAL + "-''"
FL_LEGAL = CH_LEGAL + "-''"
uFL_ILLEGAL = FL_ILLEGAL.decode('latin-1')
uFL_LEGAL = FL_LEGAL.decode('latin-1')
uFL_LEGAL = FL_LEGAL.decode('latin-1')
if not name:
return name
if isinstance(name, unicode):
illegal = uFL_ILLEGAL
legal = uFL_LEGAL
legal = uFL_LEGAL
else:
illegal = FL_ILLEGAL
legal = FL_LEGAL
legal = FL_LEGAL
lst = []
for ch in name.strip():
@@ -564,12 +660,14 @@ def sab_sanitize_foldername(name):
return name
def split_string(mystring, splitvar=','):
mylist = []
for each_word in mystring.split(splitvar):
mylist.append(each_word.strip())
return mylist
def create_https_certificates(ssl_cert, ssl_key):
"""
Stolen from SickBeard (http://github.com/midgetspy/Sick-Beard):
@@ -579,7 +677,7 @@ def create_https_certificates(ssl_cert, ssl_key):
try:
from OpenSSL import crypto
from lib.certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, serial
from certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, serial
except:
logger.warn("pyOpenSSL module missing, please install to enable HTTPS")
return False
@@ -587,18 +685,20 @@ def create_https_certificates(ssl_cert, ssl_key):
# Create the CA Certificate
cakey = createKeyPair(TYPE_RSA, 1024)
careq = createCertRequest(cakey, CN='Certificate Authority')
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60*60*24*365*10)) # ten years
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
cname = 'Headphones'
pkey = createKeyPair(TYPE_RSA, 1024)
req = createCertRequest(pkey, CN=cname)
cert = createCertificate(req, (cacert, cakey), serial, (0, 60*60*24*365*10)) # ten years
cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
# Save the key and certificate to disk
try:
open(ssl_key, 'w').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
open(ssl_cert, 'w').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
except Exception, e:
with open(ssl_key, 'w') as f:
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
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)
return False

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