Compare commits

..
343 Commits
Author SHA1 Message Date
Bas Stottelaar f15ad447ca Merge branch 'develop'
* develop:
  Release v0.5.1
  Prepare for next release
  Fixed HTML div closing tag. Updated titles
  Fixed typo
  Allow one to disable all tasks
  Allow one to adjust interval settings without restarting.
  Prevent infinite recursion while scanning library.
  Fix for new paths
  Added the real pkg_resources from setuptools 7.0. Should fix #2001
  Fixed contrib scripts. Added *.pyc cleaning scripts
  Check if pyOpenSSL is installed before enabling HTTPS.
  Ignore CSR files
  Fixed SSL too long errors in CherryPy.
  Spaces removal
  Add downgrade functionality (contributed script)
  Use urlparse to parse transmission URL
  Fix for #1998
2014-11-13 20:00:27 +01:00
Bas Stottelaar 67c29560db Release v0.5.1 2014-11-13 19:59:53 +01:00
Bas Stottelaar 8e787dca86 Prepare for next release 2014-11-13 12:11:44 +01:00
Bas Stottelaar f06dd86a99 Fixed HTML div closing tag. Updated titles 2014-11-13 12:02:07 +01:00
Bas Stottelaar 73f3cc64a5 Fixed typo 2014-11-13 11:39:55 +01:00
Bas Stottelaar 2601de5c30 Allow one to disable all tasks 2014-11-13 02:58:41 +01:00
Bas Stottelaar ee3ccfa598 Allow one to adjust interval settings without restarting. 2014-11-13 02:32:48 +01:00
Bas Stottelaar 396cbb7b64 Prevent infinite recursion while scanning library. 2014-11-13 02:32:23 +01:00
Bas Stottelaar c64f08b2b5 Fix for new paths 2014-11-12 23:36:52 +01:00
Bas Stottelaar 3cf82f4dac Added the real pkg_resources from setuptools 7.0. Should fix #2001 2014-11-12 23:31:05 +01:00
Bas Stottelaar a43256e802 Fixed contrib scripts. Added *.pyc cleaning scripts 2014-11-12 23:22:41 +01:00
Bas Stottelaar 6f91694d51 Check if pyOpenSSL is installed before enabling HTTPS.
Previous, it is only checked if pyOpenSSL is installed when certificates are
generated. Now, it is checked when HTTPS is enabled. In case it is missing,
HTTPS will be disabled.
2014-11-12 22:53:22 +01:00
Bas Stottelaar d25ce692f9 Ignore CSR files 2014-11-12 22:34:51 +01:00
Bas Stottelaar f495fc01bc Fixed SSL too long errors in CherryPy.
The fix is suggested by https://bitbucket.org/cherrypy/cherrypy/issue/1298/ssl-not-working
2014-11-12 22:29:04 +01:00
Bas Stottelaar 4aacbd2731 Spaces removal 2014-11-12 22:21:20 +01:00
Bas Stottelaar bb7b1f64ce Add downgrade functionality (contributed script) 2014-11-12 20:25:08 +01:00
Bas Stottelaar 548e0a9821 Use urlparse to parse transmission URL 2014-11-12 18:49:57 +01:00
Bas Stottelaar af23e322a1 Merge pull request #1999 from Wiltons/develop
Fix for #1998
2014-11-12 18:21:33 +01:00
Wiltons b467e2db41 Fix for #1998
Moving "rpc" addition to transmission host string after exception
2014-11-11 23:29:38 +00:00
Bas Stottelaar 13f34a17b1 Merge remote-tracking branch 'rembo10/develop'
* rembo10/develop:
  Fixed link to installation wiki
  Improved the CHANGELOG, part 3
  Improved the CHANGELOG, part 2
  Improved the CHANGELOG
  Fix for #1994
2014-11-10 21:00:47 +01:00
Bas Stottelaar d0baeec53d Fixed link to installation wiki 2014-11-10 20:56:26 +01:00
Bas Stottelaar 6cb283cdf2 Improved the CHANGELOG, part 3 2014-11-10 20:49:30 +01:00
Bas Stottelaar 43f5fe421b Improved the CHANGELOG, part 2 2014-11-10 20:48:43 +01:00
Bas Stottelaar 09c041056e Improved the CHANGELOG 2014-11-10 20:48:06 +01:00
Bas Stottelaar fda4658e92 Fix for #1994 2014-11-10 20:47:27 +01:00
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
delphiactual 9c81fbaa2b Merge pull request #1 from rembo10/develop
Develop
2014-05-05 23:23:49 -06:00
920 changed files with 34348 additions and 29484 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto
+9 -3
View File
@@ -6,12 +6,13 @@
*.pyproj
*.sln
# Logs and databases #
# Headphones files #
######################
*.log
*.db*
*.db-journal
*.ini
version.lock
logs/*
cache/*
@@ -19,6 +20,7 @@ cache/*
##################
*.crt
*.key
*.csr
# OS generated files #
######################
@@ -28,8 +30,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
+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())
+113
View File
@@ -0,0 +1,113 @@
# Changelog
## v0.5.1
Released 13 November 2014
Highlights:
* Added: allow one to disable interval tasks (#2002)
* Added: script to downgrade Headphones to last version that started (Linux only)
* Fixed: SSL issues in CherryPy. Self-generated certificates will be 2048 now (#1995)
* Fixed: Transmission URL detection (#1998)
* Fixed: missing dependencies for APScheduler and CherryPy (#2001)
* Improved: symlink infinite recursion detection
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5...v0.5.1).
## v0.5
Released 10 November 2014
Highlights:
* Added: CUE splitter
* Added: filter search result by MusicBrainz Release Group ID
* Added: follow symlinks while scanning library (#1953)
* Fixed: crash during post processing (#1897)
* Fixed: embedding lyrics (#1896)
* Fixed: HTTP errors with older versions of Python 2.6
* Fixed: jump back to top of page (#1948)
* Improved: parse MusicBrainz RGID first when post processing (#1952)
* Improved: Growl unicode characters (#1695)
* Improved: search handling for PB and KAT
* Improved: Last.FM API support (#1877)
* Improved: upgraded CherryPy to version 3.6.0
* Improved: upgraded Requests to version 2.4.1
* Improved: upgraded APScheduler to version 3.0.1
* Improved: lot of code refactoring
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.4...v0.5).
## v0.4
Released 20 September 2014
Highlights:
* Added: support for libav-tools (which replaces FFmpeg under Ubuntu)
* Added: option to freeze library when post processing
* Added: Songkick per area
* Added: rename original NFO file (#1797)
* Removed: removed dead search providers
* Fixed: removed left-overs of old packages
* Improved: rename by original folder (#1811)
* Improved: uTorrent fixess
* Improved: delayed loading of album art
* Improved: search result parser
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.3.4...v0.4).
## v0.3.4
Released 15 May 2014
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.3.3...v0.3.4).
## v0.3.3
Released 05 May 2014
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.3.2...v0.3.3).
## v0.3.2
Released 17 April 2014
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.3.1...v0.3.2).
## v0.3.1
Released 11 April 2014
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.3...v0.3.1).
## v0.3
Released 09 April 2014
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.2.3...v0.3).
## v0.2.3
Released 28 March 2014
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.2.2...v0.2.3).
## v0.2.2
Released 15 January 2014
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.2.1...v0.2.2).
## v0.2.1
Released 13 January 2014
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.2...v0.2.1).
## v0.2
Released 13 January 2014
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.1.2...v0.2).
## v0.1.2
Released 11 October 2013
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.1.1...v0.1.2).
## v0.1.1
Released 26 August 2013
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.1...v0.1.1).
## v0.1
Released 05 August 2013
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/2156e1341405d07c5bcfbe994f6b354b32d94cda...v0.1).
+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!
+92 -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,139 @@ 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,
})
# Check if pyOpenSSL is installed. It is required for certificate generation
# and for CherryPy.
if headphones.CONFIG.ENABLE_HTTPS:
try:
import OpenSSL
except ImportError:
logger.warn("The pyOpenSSL module is missing. Install this " \
"module to enable HTTPS. HTTPS will be disabled.")
headphones.CONFIG.ENABLE_HTTPS = False
if headphones.LAUNCH_BROWSER and not args.nolaunch:
headphones.launch_browser(headphones.HTTP_HOST, http_port, headphones.HTTP_ROOT)
# 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.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 +204,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 +214,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/Installation) 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.
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# Display information
echo "This script will remove *.pyc files. These files are generated by Python, but they can cause conflicts after an upgrade. It's safe to remove them, because they will be regenerated."
echo "Press enter to continue, or CTRL + C to quit."
read
# Remove the *.pyc
find "`dirname $0`/.." -type f -name "*.pyc" -exec rm -rf {} \;
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# Parameter check
if [ -z "$1" ]; then
echo "Syntax: $0 <data directory>"
exit 1
fi
# Version file check
if [ ! -s "$1/version.lock" ]; then
echo "Missing the version.lock file in the data folder, or the file is empty. Did you start Headphones at least once?"
exit 1
fi
# Git installation check
if [ ! -x "$(command -v git)" ]; then
echo "Git is required to downgrade."
exit 1
fi
# Display information
HASH=$(cat $1/version.lock)
echo "This script will try to downgrade Headphones to the last version that started, version $HASH. Make sure you have a backup of your config file and database, just in case!"
echo "Press enter to continue, or CTRL + C to quit."
read
# Downgrade
cd "`dirname $0`/.."
git reset --hard "$HASH"
echo "All done, Headphones should be downgraded to the last version that started."
-318
View File
@@ -1,318 +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.gradeI {
background-color: #bebebe;
}
table.display tr.even.gradeI {
background-color: #bebebe;
}
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

+58 -3
View File
@@ -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>
+38 -37
View File
@@ -21,17 +21,17 @@
%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>
@@ -128,7 +128,7 @@
%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
@@ -163,21 +163,21 @@
}
<%
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;
@@ -186,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();
@@ -211,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 */
@@ -240,7 +240,8 @@
refreshTable();
$('#artistnamelink').text(data["ArtistName"]);
if (loadingMessage == false){
showMsg("Getting artist information",true);
$("#ajaxMsg").after( "<div id='ajaxMsg2' class='ajaxMsg'></div>" );
showArtistMsg("Getting artist information");
loadingMessage = true;
}
if (spinner_active == false){
@@ -255,8 +256,7 @@
else{
$('#artistnamespinner').remove()
$('#loadingtext').remove()
$('#ajaxMsg').empty()
$('#ajaxMsg').removeAttr('style')
$('#ajaxMsg2').remove()
spinner_active = false
loadingtext_active = false
loadingMessage = false
@@ -265,12 +265,13 @@
}
function initThisPage() {
$('#menu_link_getextra').click(function() {
$('#menu_link_getextra').click(function(event) {
$('#dialog').dialog();
return false;
event.preventDefault();
});
$('#menu_link_modifyextra').click(function() {
$('#menu_link_modifyextra').click(function(event) {
$('#dialog').dialog();
event.preventDefault();
});
$('#album_table').dataTable({
"bDestroy": true,
@@ -303,7 +304,7 @@
resetFilters("albums");
setTimeout(function(){
initFancybox();
},1500)
},1500);
}
$(document).ready(function() {
@@ -314,7 +315,7 @@
getArtistsCalendar();
}
checkArtistStatus();
setInterval(function(){
setInterval(function(){
checkArtistStatus();
}, 1500);
});
+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;
}
-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%; }
+72 -10
View File
@@ -336,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;
}
@@ -503,7 +507,7 @@ header .wrapper {
margin: 0 auto;
overflow: hidden;
position: relative;
width: 960px;
width: 990px;
}
header #logo {
float: left;
@@ -545,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;
@@ -576,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;
@@ -595,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;
@@ -605,7 +609,7 @@ footer {
padding: 15px 10px;
text-align: left;
}
#ajaxMsg .ui-icon {
.ajaxMsg .ui-icon {
display: inline-block;
margin-left: -20px;
top: 2px;
@@ -758,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;
@@ -1126,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;
@@ -1144,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;
}
@@ -1215,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;
@@ -1416,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;
}
+35 -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");
@@ -344,12 +371,6 @@ function resetFilters(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() {
@@ -359,11 +380,6 @@ function initFancybox() {
}
}
function init() {
initHeader();
preventDefault();
}
$(document).ready(function(){
init();
initHeader();
});
+41 -43
View File
@@ -27,7 +27,7 @@
</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>
@@ -37,7 +37,6 @@
<option value="300">5 Minutes</option>
<option value="600">10 Minutes</option>
</select></div>
</%def>
<%def name="headIncludes()">
@@ -47,51 +46,50 @@
<%def name="javascriptIncludes()">
<script src="js/libs/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function() {
initActions();
$(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");
}
$('#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;
},
"fnServerData": function ( sSource, aoData, fnCallback ) {
/* Add some extra data to the sender */
$.getJSON( sSource, aoData, function (json) {
fnCallback(json)
} );
}
} );
} );
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');
+7 -7
View File
@@ -20,7 +20,7 @@
</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>
@@ -53,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>
@@ -83,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 = ''
%>
+35 -32
View File
@@ -112,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>
+23 -23
View File
@@ -5,18 +5,17 @@
<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>
@@ -41,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>
@@ -50,7 +49,7 @@
%endfor
</tbody>
</table>
</form>
</div>
<div class="title">
@@ -71,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>
@@ -89,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;
}
+226 -991
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=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [newalbumdata['ArtistID']]))
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [newalbumdata['ArtistID']]))
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)

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