diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 176a458f..00000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-* text=auto
diff --git a/.gitignore b/.gitignore
index 8bacef32..29da270b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,20 +4,18 @@
.project
.pydevproject
-# coverage generated:
+# coverage generated
/cover-html/
.coverage
.coveralls.yml
-# Compiled source #
-###################
+# Compiled source
*.pyc
*.py~
*.pyproj
*.sln
-# Headphones files #
-######################
+# Headphones files
*.log
*.db*
*.db-journal
@@ -26,27 +24,25 @@ version.lock
logs/*
cache/*
-# HTTPS Cert/Key #
-##################
+# HTTPS Cert/Key
*.crt
*.key
*.csr
-# OS generated files #
-######################
+# OS generated files
.DS_Store?
.DS_Store
ehthumbs.db
Icon?
Thumbs.db
-#Ignore files generated by PyCharm
+# Ignore files generated by PyCharm
.idea/*
-#Ignore files generated by vi
+# Ignore files generated by vi
*.swp
-#Ignore files build by Visual Studio
+# Ignore files build by Visual Studio
*.obj
*.exe
*.pdb
@@ -72,3 +68,6 @@ obj/
[Rr]elease*/
_ReSharper*/
.vscode
+
+# Python virtual env
+venv
\ No newline at end of file
diff --git a/.pep8 b/.pep8
index daa763d1..5c5608b9 100644
--- a/.pep8
+++ b/.pep8
@@ -1,15 +1,7 @@
[pep8]
-# 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 '# '
# E501 line too long (312 > 160 characters)
-# E502 the backslash is redundant between brackets
-ignore = E121,E122,E123,E124,E125,E126,E127,E128,E261,E262,E265,E501,E502
+ignore = E123,E126,E127,E128,E501
max-line-length = 160
diff --git a/.travis.yml b/.travis.yml
index 165f65e3..41ad6f25 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,36 +2,25 @@
# http://about.travis-ci.org/docs/
language: python
+
sudo: false
cache:
pip: true
directories:
- - lib
+ - lib
-# Available Python versions:
-# http://about.travis-ci.org/docs/user/ci-environment/#Python-VM-images
python:
- "2.6"
-matrix:
- include:
- - python: "2.7"
- env: SENDCOVERAGE=1
+ - "2.7"
-# pylint 1.4 does not run under python 2.6
install:
- - pip install pyOpenSSL
- - pip install pylint==1.3.1
- - pip install pyflakes
- - pip install pep8
- # coverage stuff:
- - pip install coveralls
- - pip install coverage
+ - pip install -r requirements-dev.txt
+
script:
- pep8 headphones
- pyflakes headphones
- nosetests
after_success:
- # coverage stuff:
- - if [ $SENDCOVERAGE ]; then coveralls; fi
+ - if [[ $TRAVIS_PYTHON_VERSION == "2.7" ]]; then coveralls; fi
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e8d45ae..f4bb2bd4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## v0.5.14
+Released 02 June 2016
+
+Highlights:
+* Fixed: File/folder format on new installs
+* Fixed: Pep8 errors
+* Improved: Updated fontawesome
+* Improved: Reverted back to less
+
+The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.13...v0.5.14).
+
## v0.5.13
Released 25 February 2016
diff --git a/README.md b/README.md
index 87eb2f36..ff1e214b 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
## Headphones
-**Master Branch:** [](https://travis-ci.org/rembo10/headphones)
-**Develop Branch:** [](https://travis-ci.org/rembo10/headphones)
+[](https://travis-ci.org/rembo10/headphones)
+[](https://travis-ci.org/rembo10/headphones)
Headphones is an automated music downloader for NZB and Torrent, written in Python. It supports SABnzbd, NZBget, Transmission, µTorrent, Deluge and Blackhole.
diff --git a/data/interfaces/default/base.html b/data/interfaces/default/base.html
index f95be978..9dfa8a9d 100644
--- a/data/interfaces/default/base.html
+++ b/data/interfaces/default/base.html
@@ -82,6 +82,9 @@
diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html
index 9ba9f2aa..ffd6e68d 100644
--- a/data/interfaces/default/config.html
+++ b/data/interfaces/default/config.html
@@ -392,6 +392,12 @@
Usually http://localhost:8112 (requires WebUI plugin)
+
+ Deluge SSL Certificate
+
+ Path to the certificate file. Make sure to use a valid certificate ("Issued To" field must match
+ hostname) which is not the case with the default certificate. Path is usually %appdata%\deluge\ssl on Windows, ~/.config/deluge/ssl/ on Linux. Leave this blank if you are using a self-signed certificate.
+
Deluge Password
@@ -888,7 +894,7 @@
as .jpg
-
Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in square brackets, use single-quote marks to escape square brackets literally ('[', ']').
+
Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').
@@ -1283,13 +1289,13 @@
Folder Format
- Use: $Artist/$artist, $SortArtist/$sortartist, $Album/$album, $Year/$year, $Type/$type (release type) and $First/$first (first letter in artist name), $OriginalFolder/$originalfolder (downloaded directory name). Put optional variables in square brackets, use single-quote marks to escape square brackets literally ('[', ']'). E.g.: $Type/$First/$artist/$album '['$year']' = Album/G/girl talk/all day [2010]
+ Use: $Artist/$artist, $SortArtist/$sortartist, $Album/$album, $Year/$year, $Type/$type (release type) and $First/$first (first letter in artist name), $OriginalFolder/$originalfolder (downloaded directory name). Put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}'). E.g.: $Type/$First/$artist/$album{ '['$year']'} = Album/G/girl talk/all day [2010]
File Format
- Use: $Disc/$disc (disc #), $Track/$track (track #), $Title/$title, $Artist/$artist, $Album/$album and $Year/$year. Put optional variables in square brackets, use single-quote marks to escape square brackets literally ('[', ']').
+ Use: $Disc/$disc (disc #), $Track/$track (track #), $Title/$title, $Artist/$artist, $Album/$album and $Year/$year. Put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').
Use underscores instead of spaces
@@ -1592,7 +1598,7 @@
Musicbrainz
-
Muscbrainz Mirror
+
Musicbrainz Mirror
%for mirror in config['mirrorlist']:
<%
diff --git a/data/interfaces/default/css/README.md b/data/interfaces/default/css/README.md
new file mode 100644
index 00000000..c98ffb94
--- /dev/null
+++ b/data/interfaces/default/css/README.md
@@ -0,0 +1,25 @@
+# About less and css
+
+**The main message of this document: DO NOT MODIFY style.css - IT IS GENERATED FILE**
+
+----
+
+If you want modify the appearance of Headphones, please, follow this simple steps.
+
+0. **Install**. Do this just once, and if you still do not have `lessc` utility on your PC. Here is very useful guide, how to install less : [http://lesscss.org/#using-less-installation](http://lesscss.org/#using-less-installation)
+1. **Modify**. Carefully add your changes to the `style.less` (`.less` extension, not `.css`).
+2. **Compile**. Currently, there is no magic, so you should compile css manually. Go to the `/data/interfaces/default/css` folder (or use full paths..), and then just type:
+
+```bash
+lessc style.less > style.css
+```
+
+_works good on *nix hosts, I didn't test this on win-hosts_
+
+DONE. You have new CSS file.
+
+## LESS
+
+Less is very useful tool (CSS pre-processor) for CSS writing. There is the awesome guide on the official site: [Official Less Guide](http://lesscss.org/features/)
+
+Thanks!
\ No newline at end of file
diff --git a/data/interfaces/default/css/config.less b/data/interfaces/default/css/config.less
index 022a0700..274cb6d8 100644
--- a/data/interfaces/default/css/config.less
+++ b/data/interfaces/default/css/config.less
@@ -10,9 +10,16 @@
@msg-bg: #FFF6A9;
@msg-bg-success: #D3FFD7;
@msg-bg-error: #FFD3D3;
+@brand-color: #4F4F4F;
+@caption-color: #999999;
+
+@gradient-color-1: #FAFAFA;
+@gradient-color-2: #E7E7E7;
+
+@heading-input-radius: 5px;
/* Mixins */
-.rounded(@radius: 5px) {
+.rounded(@radius: 5px) {
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
@@ -75,3 +82,32 @@
opacity:@opacity_percent / 100 !important;
}
+
+/*
+ COLUMNS STUFF
+ https://css-tricks.com/guide-responsive-friendly-css-columns/
+*/
+.columnWidth(@width) {
+ -webkit-column-width: @width;
+ -moz-column-width: @width;
+ column-width: @width;
+}
+.columnCount(@count) {
+ -webkit-columns: @count;
+ -moz-columns: @count;
+ columns: @count;
+}
+.columnGap(@width) {
+ -webkit-column-gap: @width;
+ -moz-column-gap: @width;
+ column-gap: @width;
+}
+.columnRule(@border) {
+ -webkit-column-rule: @border;
+ -moz-column-rule: @border;
+ column-rule: @border;
+}
+.columnSpan(@what) {
+ -webkit-column-span: @what;
+ column-span: @what;
+}
\ No newline at end of file
diff --git a/data/interfaces/default/css/font-awesome.min.css b/data/interfaces/default/css/font-awesome.min.css
index 449d6ac5..8e3562d8 100644
--- a/data/interfaces/default/css/font-awesome.min.css
+++ b/data/interfaces/default/css/font-awesome.min.css
@@ -1,4 +1,4 @@
/*!
- * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome
+ * Font Awesome 4.6.1 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
- */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}
\ No newline at end of file
+ */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.6.1');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.6.1') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.6.1') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.6.1') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.6.1') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.6.1#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
\ No newline at end of file
diff --git a/data/interfaces/default/css/style.css b/data/interfaces/default/css/style.css
index 855976bc..8227537d 100644
--- a/data/interfaces/default/css/style.css
+++ b/data/interfaces/default/css/style.css
@@ -1,5 +1,9 @@
/* Variables */
/* Mixins */
+/*
+ COLUMNS STUFF
+ https://css-tricks.com/guide-responsive-friendly-css-columns/
+*/
html,
body,
div,
@@ -103,7 +107,7 @@ body {
padding: 0;
}
a {
- color: #4183c4;
+ color: #4183C4;
text-decoration: none;
outline: none;
}
@@ -125,7 +129,7 @@ a .ui-icon {
float: left;
}
.links a:hover {
- color: #4183c4;
+ color: #4183C4;
}
.links a .ui-icon {
float: left;
@@ -146,14 +150,106 @@ p.center {
}
hr {
border: 0;
- border-top: 1px solid #cccccc;
+ border-top: 1px solid #CCCCCC;
display: block;
height: 1px;
margin: 1em 0;
padding: 0;
}
-small {
- font-size: 85%;
+small,
+.small {
+ font-size: xx-small;
+}
+.configtable legend {
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 10px;
+ text-shadow: 1px 1px 0 #FFFFFF;
+}
+.configtable tr td:last-child {
+ border-left: 1px dotted #ddd;
+ padding-left: 20px;
+}
+.configtable td {
+ padding-right: 15px;
+ width: 50%;
+}
+.config legend {
+ font-size: larger;
+ font-weight: bold;
+ margin-bottom: 1em;
+ text-shadow: 1px 1px 0 #FFFFFF;
+}
+.config .caption {
+ color: #999999;
+ line-height: 12px;
+ margin-top: 3px;
+}
+.config .tab {
+ -webkit-column-rule: 1px solid #CCCCCC;
+ -moz-column-rule: 1px solid #CCCCCC;
+ column-rule: 1px solid #CCCCCC;
+ -webkit-columns: 2;
+ -moz-columns: 2;
+ columns: 2;
+ -webkit-column-gap: 4em;
+ -moz-column-gap: 4em;
+ column-gap: 4em;
+}
+.config .tab .message {
+ -webkit-column-span: all;
+ column-span: all;
+ display: block;
+ background-color: transparent;
+ margin-bottom: 1em;
+}
+.config .block {
+ -webkit-column-break-inside: avoid;
+ page-break-inside: avoid;
+ break-inside: avoid-column;
+}
+.config .option {
+ margin-bottom: 1em;
+ font-size: 12px;
+}
+.config .option:after {
+ content: " ";
+ /* Older browser do not support empty content */
+ visibility: hidden;
+ display: block;
+ height: 0;
+ clear: both;
+}
+.config .option .input-wrapper {
+ float: left;
+}
+.config .option label {
+ width: 170px;
+ font-size: 12px;
+ float: left;
+}
+.config .option input {
+ margin-right: 5px;
+}
+.config .option input[type=text],
+.config .option input[type=password],
+.config .option input[type=number] {
+ border: 1px solid #DDD;
+ border-top: 1px solid #CDCDCD;
+ -moz-box-shadow: inset 0 1px 1px #e0e0e0;
+ -webkit-box-shadow: inset 0 1px 1px #e0e0e0;
+ -o-box-shadow: inset 0 1px 1px #e0e0e0;
+ box-shadow: inset 0 1px 1px #e0e0e0;
+ color: #343434;
+ font-size: larger;
+ max-width: 230px;
+ padding: 3px 5px;
+}
+.config .option input[type=number] {
+ max-width: 5em;
+}
+.config .embed-option-block {
+ margin-left: 1em;
}
img.albumArt {
float: left;
@@ -184,17 +280,17 @@ table {
border-spacing: 0;
}
table th {
- background-image: -moz-linear-gradient(#fafafa, #eaeaea) !important;
- background-image: linear-gradient(#fafafa, #eaeaea) !important;
- background-image: -webkit-linear-gradient(#fafafa, #eaeaea) !important;
- background-image: -o-linear-gradient(#fafafa, #eaeaea) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important;
+ background-image: -moz-linear-gradient(#FAFAFA, #E7E7E7) !important;
+ background-image: linear-gradient(#FAFAFA, #E7E7E7) !important;
+ background-image: -webkit-linear-gradient(#FAFAFA, #E7E7E7) !important;
+ background-image: -o-linear-gradient(#FAFAFA, #E7E7E7) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
border-left: 1px solid #E0E0E0;
- -moz-box-shadow: 1px 0 0 #fafafa;
- -webkit-box-shadow: 1px 0 0 #fafafa;
- -o-box-shadow: 1px 0 0 #fafafa;
- box-shadow: 1px 0 0 #fafafa;
+ -moz-box-shadow: 1px 0 0 #FAFAFA;
+ -webkit-box-shadow: 1px 0 0 #FAFAFA;
+ -o-box-shadow: 1px 0 0 #FAFAFA;
+ box-shadow: 1px 0 0 #FAFAFA;
text-shadow: 1px 1px 0 #FFFFFF ;
}
table th input[type="checkbox"] {
@@ -209,13 +305,13 @@ table th:first-child {
}
table th.sorting_desc,
table th.sorting_asc {
- background-image: -moz-linear-gradient(#fafbfd, #dce6ef) !important;
- background-image: linear-gradient(#fafbfd, #dce6ef) !important;
- background-image: -webkit-linear-gradient(#fafbfd, #dce6ef) !important;
- background-image: -o-linear-gradient(#fafbfd, #dce6ef) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafbfd, endColorstr=#dce6ef) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafbfd, endColorstr=#dce6ef) !important;
- color: #4183c4;
+ background-image: -moz-linear-gradient(#FAFBFD, #DCE6EF) !important;
+ background-image: linear-gradient(#FAFBFD, #DCE6EF) !important;
+ background-image: -webkit-linear-gradient(#FAFBFD, #DCE6EF) !important;
+ background-image: -o-linear-gradient(#FAFBFD, #DCE6EF) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFBFD, endColorstr=#DCE6EF) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFBFD, endColorstr=#DCE6EF) !important;
+ color: #4183C4;
}
table td {
vertical-align: top;
@@ -227,18 +323,7 @@ select,
input,
textarea,
button {
- font: 99%;
-}
-select {
- -moz-border-radius: 5px;
- -webkit-border-radius: 5px;
- border-radius: 5px;
- background: #4F4F4F;
- border: 0;
- border-bottom: 1px solid rgba(0, 0, 0, 0.25);
- color: #fff;
- padding: 3px 10px;
- text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.25);
+ font-size: 99%;
}
textarea {
overflow: auto;
@@ -286,8 +371,7 @@ input,
select,
form .checkbox input,
.configtable td#middle,
-#artist_table td#have,
-#album_table td#have {
+#artist_table td#have {
vertical-align: middle;
}
input[type="radio"] {
@@ -336,7 +420,6 @@ form fieldset small.heading {
margin-top: -15px;
}
form .row {
- font-family: Helvetica, Arial, sans-serif;
margin-bottom: 10px;
}
form .row label {
@@ -416,21 +499,6 @@ form .checkbox small {
margin: 0 !important;
width: auto;
}
-form .indent input {
- margin-left: 15px;
-}
-form .suboptions {
- margin-left: 15px;
-}
-.option{
- font-size: 15px;
- font-weight: 600;
- vertical-align: middle;
-}
-input.bigcheck[type="checkbox"] {
- width: 16px;
- height: 16px;
-}
ul,
ol {
margin-left: 2em;
@@ -473,24 +541,24 @@ ul#nav li a {
font-family: "Trebuchet MS", Helvetica, Arial, sans-serif;
}
ul#nav li a:hover {
- background-image: -moz-linear-gradient(#f1f1f1, #e0e0e0) !important;
- background-image: linear-gradient(#f1f1f1, #e0e0e0) !important;
- background-image: -webkit-linear-gradient(#f1f1f1, #e0e0e0) !important;
- background-image: -o-linear-gradient(#f1f1f1, #e0e0e0) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f1f1f1, endColorstr=#e0e0e0) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f1f1f1, endColorstr=#e0e0e0) !important;
+ background-image: -moz-linear-gradient(#F1F1F1, #E0E0E0) !important;
+ background-image: linear-gradient(#F1F1F1, #E0E0E0) !important;
+ background-image: -webkit-linear-gradient(#F1F1F1, #E0E0E0) !important;
+ background-image: -o-linear-gradient(#F1F1F1, #E0E0E0) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#F1F1F1, endColorstr=#E0E0E0) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#F1F1F1, endColorstr=#E0E0E0) !important;
border: 1px solid #DDDDDD;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
- -moz-box-shadow: 0 1px 0 #fafafa;
- -webkit-box-shadow: 0 1px 0 #fafafa;
- -o-box-shadow: 0 1px 0 #fafafa;
- box-shadow: 0 1px 0 #fafafa;
- -moz-box-shadow: 0 1px 0 #fafafa inset;
- -webkit-box-shadow: 0 1px 0 #fafafa inset;
- -o-box-shadow: 0 1px 0 #fafafa inset;
- box-shadow: 0 1px 0 #fafafa inset;
+ -moz-box-shadow: 0 1px 0 #FAFAFA;
+ -webkit-box-shadow: 0 1px 0 #FAFAFA;
+ -o-box-shadow: 0 1px 0 #FAFAFA;
+ box-shadow: 0 1px 0 #FAFAFA;
+ -moz-box-shadow: 0 1px 0 #FAFAFA inset;
+ -webkit-box-shadow: 0 1px 0 #FAFAFA inset;
+ -o-box-shadow: 0 1px 0 #FAFAFA inset;
+ box-shadow: 0 1px 0 #FAFAFA inset;
-webkit-transition: color 0.2s ease-in;
-moz-transition: color 0.2s ease-in;
-o-transition: color 0.2s ease-in;
@@ -510,13 +578,13 @@ ul#nav li a.log {
padding: 10px 15px 11px;
}
header {
- background-image: -moz-linear-gradient(#fafafa, #eaeaea) !important;
- background-image: linear-gradient(#fafafa, #eaeaea) !important;
- background-image: -webkit-linear-gradient(#fafafa, #eaeaea) !important;
- background-image: -o-linear-gradient(#fafafa, #eaeaea) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important;
- border-bottom: 1px solid #CACACA;
+ background-image: -moz-linear-gradient(#FAFAFA, #E7E7E7) !important;
+ background-image: linear-gradient(#FAFAFA, #E7E7E7) !important;
+ background-image: -webkit-linear-gradient(#FAFAFA, #E7E7E7) !important;
+ background-image: -o-linear-gradient(#FAFAFA, #E7E7E7) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
+ border-bottom: 1px solid #CCCCCC;
-moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
-o-box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
@@ -556,12 +624,12 @@ footer {
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
border-radius: 10px;
- background-image: -moz-linear-gradient(#fcf5c2, #fff6a9) !important;
- background-image: linear-gradient(#fcf5c2, #fff6a9) !important;
- background-image: -webkit-linear-gradient(#fcf5c2, #fff6a9) !important;
- background-image: -o-linear-gradient(#fcf5c2, #fff6a9) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important;
+ background-image: -moz-linear-gradient(#FCF5C2, #FFF6A9) !important;
+ background-image: linear-gradient(#FCF5C2, #FFF6A9) !important;
+ background-image: -webkit-linear-gradient(#FCF5C2, #FFF6A9) !important;
+ background-image: -o-linear-gradient(#FCF5C2, #FFF6A9) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FCF5C2, endColorstr=#FFF6A9) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FCF5C2, endColorstr=#FFF6A9) !important;
display: inline-block;
padding: 5px 10px;
margin-top: 10px;
@@ -573,23 +641,23 @@ footer {
top: 4px;
}
.ajaxMsg {
- border: 1px solid #cccccc;
- background-image: -moz-linear-gradient(#ffffff, #eeeeee) !important;
- background-image: linear-gradient(#ffffff, #eeeeee) !important;
- background-image: -webkit-linear-gradient(#ffffff, #eeeeee) !important;
- background-image: -o-linear-gradient(#ffffff, #eeeeee) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important;
+ border: 1px solid #CCCCCC;
+ background-image: -moz-linear-gradient(#FFFFFF, #EEEEEE) !important;
+ background-image: linear-gradient(#FFFFFF, #EEEEEE) !important;
+ background-image: -webkit-linear-gradient(#FFFFFF, #EEEEEE) !important;
+ background-image: -o-linear-gradient(#FFFFFF, #EEEEEE) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFFFFF, endColorstr=#EEEEEE) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFFFFF, endColorstr=#EEEEEE) !important;
-moz-border-radius: 7px;
-webkit-border-radius: 7px;
border-radius: 7px;
display: none;
- font-size: 14px;
+ font-size: larger;
right: 10px;
- -moz-box-shadow: 0px 0px 2px #aaaaaa;
- -webkit-box-shadow: 0px 0px 2px #aaaaaa;
- -o-box-shadow: 0px 0px 2px #aaaaaa;
- box-shadow: 0px 0px 2px #aaaaaa;
+ -moz-box-shadow: 0px 0px 2px #aaa;
+ -webkit-box-shadow: 0px 0px 2px #aaa;
+ -o-box-shadow: 0px 0px 2px #aaa;
+ box-shadow: 0px 0px 2px #aaa;
padding: 7px 10px;
position: fixed;
text-align: center;
@@ -612,23 +680,23 @@ footer {
position: relative;
top: 2px;
}
-.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;
- background-image: -o-linear-gradient(#d3ffd7, #c2edc6) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important;
+.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;
+ background-image: -o-linear-gradient(#D3FFD7, #C2EDC6) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#D3FFD7, endColorstr=#C2EDC6) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#D3FFD7, endColorstr=#C2EDC6) !important;
padding: 15px 10px;
text-align: left;
}
-.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;
- background-image: -o-linear-gradient(#ffd3d3, #edc4c4) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important;
+.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;
+ background-image: -o-linear-gradient(#FFD3D3, #EDC4C4) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFD3D3, endColorstr=#EDC4C4) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFD3D3, endColorstr=#EDC4C4) !important;
padding: 15px 10px;
text-align: left;
}
@@ -640,23 +708,23 @@ footer {
margin-right: 3px;
}
#updatebar {
- border: 1px solid #cccccc;
- background-image: -moz-linear-gradient(#ffffff, #eeeeee) !important;
- background-image: linear-gradient(#ffffff, #eeeeee) !important;
- background-image: -webkit-linear-gradient(#ffffff, #eeeeee) !important;
- background-image: -o-linear-gradient(#ffffff, #eeeeee) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important;
+ border: 1px solid #CCCCCC;
+ background-image: -moz-linear-gradient(#FFFFFF, #EEEEEE) !important;
+ background-image: linear-gradient(#FFFFFF, #EEEEEE) !important;
+ background-image: -webkit-linear-gradient(#FFFFFF, #EEEEEE) !important;
+ background-image: -o-linear-gradient(#FFFFFF, #EEEEEE) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFFFFF, endColorstr=#EEEEEE) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFFFFF, endColorstr=#EEEEEE) !important;
-moz-border-radius: 7px;
-webkit-border-radius: 7px;
border-radius: 7px;
display: none;
- font-size: 14px;
+ font-size: larger;
right: 10px;
- -moz-box-shadow: 0px 0px 2px #aaaaaa;
- -webkit-box-shadow: 0px 0px 2px #aaaaaa;
- -o-box-shadow: 0px 0px 2px #aaaaaa;
- box-shadow: 0px 0px 2px #aaaaaa;
+ -moz-box-shadow: 0px 0px 2px #aaa;
+ -webkit-box-shadow: 0px 0px 2px #aaa;
+ -o-box-shadow: 0px 0px 2px #aaa;
+ box-shadow: 0px 0px 2px #aaa;
padding: 7px 10px;
position: fixed;
text-align: center;
@@ -670,12 +738,12 @@ footer {
-o-opacity: 0.8 !important;
opacity: 0.8 !important;
display: block;
- background-image: -moz-linear-gradient(#fcf5c2, #fff6a9) !important;
- background-image: linear-gradient(#fcf5c2, #fff6a9) !important;
- background-image: -webkit-linear-gradient(#fcf5c2, #fff6a9) !important;
- background-image: -o-linear-gradient(#fcf5c2, #fff6a9) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important;
+ background-image: -moz-linear-gradient(#FCF5C2, #FFF6A9) !important;
+ background-image: linear-gradient(#FCF5C2, #FFF6A9) !important;
+ background-image: -webkit-linear-gradient(#FCF5C2, #FFF6A9) !important;
+ background-image: -o-linear-gradient(#FCF5C2, #FFF6A9) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FCF5C2, endColorstr=#FFF6A9) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FCF5C2, endColorstr=#FFF6A9) !important;
}
#updatebar .msg {
font-family: "Trebuchet MS", Helvetica, Arial, sans-serif;
@@ -687,22 +755,22 @@ footer {
top: 2px;
}
#updatebar.success {
- background-image: -moz-linear-gradient(#d3ffd7, #c2edc6) !important;
- background-image: linear-gradient(#d3ffd7, #c2edc6) !important;
- background-image: -webkit-linear-gradient(#d3ffd7, #c2edc6) !important;
- background-image: -o-linear-gradient(#d3ffd7, #c2edc6) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important;
+ background-image: -moz-linear-gradient(#D3FFD7, #C2EDC6) !important;
+ background-image: linear-gradient(#D3FFD7, #C2EDC6) !important;
+ background-image: -webkit-linear-gradient(#D3FFD7, #C2EDC6) !important;
+ background-image: -o-linear-gradient(#D3FFD7, #C2EDC6) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#D3FFD7, endColorstr=#C2EDC6) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#D3FFD7, endColorstr=#C2EDC6) !important;
padding: 15px 10px;
text-align: left;
}
#updatebar.error {
- background-image: -moz-linear-gradient(#ffd3d3, #edc4c4) !important;
- background-image: linear-gradient(#ffd3d3, #edc4c4) !important;
- background-image: -webkit-linear-gradient(#ffd3d3, #edc4c4) !important;
- background-image: -o-linear-gradient(#ffd3d3, #edc4c4) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important;
+ background-image: -moz-linear-gradient(#FFD3D3, #EDC4C4) !important;
+ background-image: linear-gradient(#FFD3D3, #EDC4C4) !important;
+ background-image: -webkit-linear-gradient(#FFD3D3, #EDC4C4) !important;
+ background-image: -o-linear-gradient(#FFD3D3, #EDC4C4) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFD3D3, endColorstr=#EDC4C4) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFD3D3, endColorstr=#EDC4C4) !important;
padding: 15px 10px;
text-align: left;
}
@@ -731,23 +799,23 @@ footer {
z-index: 99;
}
#subhead #subhead_container #subhead_menu a {
- background-image: -moz-linear-gradient(#f4f4f4, #e7e7e7) !important;
- background-image: linear-gradient(#f4f4f4, #e7e7e7) !important;
- background-image: -webkit-linear-gradient(#f4f4f4, #e7e7e7) !important;
- background-image: -o-linear-gradient(#f4f4f4, #e7e7e7) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f4f4f4, endColorstr=#e7e7e7) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f4f4f4, endColorstr=#e7e7e7) !important;
+ background-image: -moz-linear-gradient(#FAFAFA, #E7E7E7) !important;
+ background-image: linear-gradient(#FAFAFA, #E7E7E7) !important;
+ background-image: -webkit-linear-gradient(#FAFAFA, #E7E7E7) !important;
+ background-image: -o-linear-gradient(#FAFAFA, #E7E7E7) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
font-size: 12px;
font-weight: normal;
}
#subhead #subhead_container #subhead_menu a:hover {
- background-image: -moz-linear-gradient(#599bdc, #3072b3) !important;
- background-image: linear-gradient(#599bdc, #3072b3) !important;
- background-image: -webkit-linear-gradient(#599bdc, #3072b3) !important;
- background-image: -o-linear-gradient(#599bdc, #3072b3) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#599bdc, endColorstr=#3072b3) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#599bdc, endColorstr=#3072b3) !important;
+ background-image: -moz-linear-gradient(#599BDC, #3072B3) !important;
+ background-image: linear-gradient(#599BDC, #3072B3) !important;
+ background-image: -webkit-linear-gradient(#599BDC, #3072B3) !important;
+ background-image: -o-linear-gradient(#599BDC, #3072B3) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#599BDC, endColorstr=#3072B3) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#599BDC, endColorstr=#3072B3) !important;
color: #FFF;
border-color: #518CC6 #518CC6 #2A65A0;
}
@@ -770,6 +838,9 @@ div#searchbar {
div#searchbar input[type=text] {
border: 1px solid #DDD;
border-top: 1px solid #CDCDCD;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ border-radius: 5px;
-moz-box-shadow: 0 1px 0 #f1f1f1;
-webkit-box-shadow: 0 1px 0 #f1f1f1;
-o-box-shadow: 0 1px 0 #f1f1f1;
@@ -778,9 +849,9 @@ div#searchbar input[type=text] {
-webkit-box-shadow: inset 0 1px 1px #e0e0e0;
-o-box-shadow: inset 0 1px 1px #e0e0e0;
box-shadow: inset 0 1px 1px #e0e0e0;
- color: #999;
+ color: #999999;
float: left;
- font-size: 14px;
+ font-size: larger;
height: auto;
line-height: normal;
margin-right: 5px;
@@ -794,20 +865,6 @@ div#searchbar .mini-icon {
margin-left: 9px;
margin-top: 7px;
}
-.configtable legend {
- font-size: 16px;
- font-weight: bold;
- margin-bottom: 10px;
- text-shadow: 1px 1px 0 #FFFFFF;
-}
-.configtable tr td:last-child {
- border-left: 1px dotted #ddd;
- padding-left: 20px;
-}
-.configtable td {
- padding-right: 15px;
- width: 50%;
-}
.table_wrapper {
_height: 302px;
background-color: #FFF;
@@ -851,10 +908,10 @@ div#artistheader {
div#artistheader #artistImg {
background: #ffffff url("../images/loader_black.gif") center no-repeat;
border: 5px solid #FFF;
- -moz-box-shadow: 1px 1px 2px 0 #555555;
- -webkit-box-shadow: 1px 1px 2px 0 #555555;
- -o-box-shadow: 1px 1px 2px 0 #555555;
- box-shadow: 1px 1px 2px 0 #555555;
+ -moz-box-shadow: 1px 1px 2px 0 #555;
+ -webkit-box-shadow: 1px 1px 2px 0 #555;
+ -o-box-shadow: 1px 1px 2px 0 #555;
+ box-shadow: 1px 1px 2px 0 #555;
float: left;
height: 200px;
margin-bottom: 30px;
@@ -918,7 +975,7 @@ div#artistheader h2 a {
vertical-align: middle;
}
#artist_table #artistImg {
- background: url("../images/loader_black.gif") no-repeat scroll center center #ffffff;
+ background: url("../images/loader_black.gif") no-repeat scroll center center #FFFFFF;
border: 3px solid #FFFFFF;
box-shadow: 1px 1px 2px 0 #555555;
float: left;
@@ -970,10 +1027,10 @@ div#artistheader h2 a {
#albumheader #albumImg {
background: #ffffff url("../images/loader_black.gif") center no-repeat;
border: 5px solid #FFF;
- -moz-box-shadow: 1px 1px 2px 0 #555555;
- -webkit-box-shadow: 1px 1px 2px 0 #555555;
- -o-box-shadow: 1px 1px 2px 0 #555555;
- box-shadow: 1px 1px 2px 0 #555555;
+ -moz-box-shadow: 1px 1px 2px 0 #555;
+ -webkit-box-shadow: 1px 1px 2px 0 #555;
+ -o-box-shadow: 1px 1px 2px 0 #555;
+ box-shadow: 1px 1px 2px 0 #555;
float: left;
height: 200px;
margin-bottom: 30px;
@@ -1016,6 +1073,13 @@ div#artistheader h2 a {
#albumheader .albuminfo li:last-child {
border: none;
}
+.nopad {
+ margin-bottom: 0 !important;
+ margin-top: 0 !important;
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ padding: 0 !important;
+}
#album_table {
background-color: #FFF;
}
@@ -1032,7 +1096,6 @@ div#artistheader h2 a {
text-align: center;
width: 175px;
}
-#album_table th#status,
#album_table th#albumart {
min-width: 50px;
text-align: left;
@@ -1047,11 +1110,47 @@ div#artistheader h2 a {
text-align: center;
width: 80px;
}
+#album_table th#albumname {
+ min-width: 150px;
+ text-align: center;
+}
+#album_table th#bitrate,
+#album_table th#albumformat {
+ min-width: 60px;
+ text-align: center;
+}
+#album_table th#type {
+ min-width: 100px;
+ text-align: center;
+ width: 175px;
+}
+#album_table td {
+ vertical-align: middle;
+ font-size: larger;
+}
+#album_table td#albumname,
+#album_table td#reldate,
+#album_table td#type,
+#album_table td#bitrate,
+#album_table td#albumformat,
+#album_table td#score,
+#album_table td#wantlossless {
+ text-align: center;
+}
+#album_table td#select {
+ text-align: left;
+}
+#album_table td#albumart {
+ text-align: left;
+}
#album_table td#albumart img {
background: #FFF;
border: 1px solid #ccc;
padding: 3px;
}
+#album_table td#status {
+ text-align: center;
+}
#album_table td#status a#wantlossless {
white-space: nowrap;
}
@@ -1094,20 +1193,19 @@ div#artistheader h2 a {
text-align: center;
vertical-align: middle;
}
-#downloads_table th#title {
+#downloads_table th {
text-align: center;
+}
+#downloads_table th#title {
min-width: 500px;
}
#downloads_table th#size {
- text-align: center;
min-width: 80px;
}
#downloads_table th#provider {
- text-align: center;
min-width: 100px;
}
#downloads_table th#kind {
- text-align: center;
min-width: 80px;
}
#history_table {
@@ -1152,87 +1250,54 @@ div#artistheader h2 a {
font-size: 12px;
padding: 2px 10px;
}
+#searchresults_table th {
+ font-size: larger;
+ text-align: left;
+ min-width: 50px;
+}
+#searchresults_table th#artistnamesmall {
+ min-width: 125px;
+}
+#searchresults_table th#reldate {
+ min-width: 80px;
+ text-align: center;
+}
#searchresults_table th#albumname {
min-width: 250px;
- text-align: left;
- font-size: 14px;
}
#searchresults_table th#artistname {
min-width: 325px;
+}
+#searchresults_table th#score {
+ min-width: 75px;
+ text-align: center;
+}
+#searchresults_table td {
+ vertical-align: middle;
text-align: left;
+ font-size: larger;
+ min-width: 50px;
}
-#searchresults_table th#artistnamesmall {
- min-width: 125px;
- text-align: left;
- font-size: 14px;
+#searchresults_table td#artistnamesmall {
+ min-width: 100px;
}
-#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;
- box-shadow: 1px 1px 2px 0 #555555;
- float: left;
- height: 50px;
- overflow: hidden;
- text-indent: -3000px;
- width: 50px;
+#searchresults_table td#reldate {
+ min-width: 80px;
+ text-align: center;
}
#searchresults_table td#albumname {
- min-width: 250px;
- text-align: left;
- vertical-align: middle;
- font-size: 14px;
+ min-width: 220px;
}
#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;
+#searchresults_table td#score {
+ min-width: 75px;
}
#searchresults_table td#score .bar {
width: 100px;
margin: 0 auto;
- border: 1px solid #cccccc;
+ border: 1px solid #CCCCCC;
padding: 1px;
background-color: #FFF;
}
@@ -1243,12 +1308,22 @@ div#artistheader h2 a {
font-size: 11px;
vertical-align: middle;
line-height: normal;
- background-image: -moz-linear-gradient(#a3e532, #90cc2a) !important;
- background-image: linear-gradient(#a3e532, #90cc2a) !important;
- background-image: -webkit-linear-gradient(#a3e532, #90cc2a) !important;
- background-image: -o-linear-gradient(#a3e532, #90cc2a) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important;
+ background-image: -moz-linear-gradient(#A3E532, #90CC2A) !important;
+ background-image: linear-gradient(#A3E532, #90CC2A) !important;
+ background-image: -webkit-linear-gradient(#A3E532, #90CC2A) !important;
+ background-image: -o-linear-gradient(#A3E532, #90CC2A) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#A3E532, endColorstr=#90CC2A) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#A3E532, endColorstr=#90CC2A) !important;
+}
+#searchresults_table #artistImg {
+ background: url("../images/loader_black.gif") no-repeat scroll center center #FFFFFF;
+ border: 3px solid #FFFFFF;
+ box-shadow: 1px 1px 2px 0 #555555;
+ float: left;
+ height: 50px;
+ overflow: hidden;
+ text-indent: -3000px;
+ width: 50px;
}
.progress-container {
background: #FFF;
@@ -1260,12 +1335,12 @@ div#artistheader h2 a {
width: 100px;
}
.progress-container > div {
- background-image: -moz-linear-gradient(#a3e532, #90cc2a) !important;
- background-image: linear-gradient(#a3e532, #90cc2a) !important;
- background-image: -webkit-linear-gradient(#a3e532, #90cc2a) !important;
- background-image: -o-linear-gradient(#a3e532, #90cc2a) !important;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important;
- -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important;
+ background-image: -moz-linear-gradient(#A3E532, #90CC2A) !important;
+ background-image: linear-gradient(#A3E532, #90CC2A) !important;
+ background-image: -webkit-linear-gradient(#A3E532, #90CC2A) !important;
+ background-image: -o-linear-gradient(#A3E532, #90CC2A) !important;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#A3E532, endColorstr=#90CC2A) !important;
+ -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#A3E532, endColorstr=#90CC2A) !important;
height: 14px;
}
.havetracks {
@@ -1293,14 +1368,14 @@ div#artistheader h2 a {
text-align: left;
margin-left: 10px;
margin-top: -5px;
- color: #cccccc;
+ color: #CCCCCC;
}
#actions {
float: right;
text-align: right;
margin-right: 10px;
margin-top: -5px;
- color: #cccccc;
+ color: #CCCCCC;
}
#actions .ui-icon {
position: relative;
@@ -1324,7 +1399,9 @@ div#artistheader h2 a {
position: relative;
top: 7px;
}
-#trashcan {margin-top:15px;}
+#trashcan {
+ margin-top: 15px;
+}
.cloudtag {
font-size: 16px;
}
@@ -1421,78 +1498,6 @@ div#artistheader h2 a {
.invisible {
visibility: hidden;
}
-.clearfix:before,
-.clearfix:after {
- content: "\0020";
- display: block;
- height: 0;
- overflow: hidden;
-}
-.clearfix {
- zoom: 1px;
-}
-.clearfix:after {
- clear: both;
-}
-.override-float {
- float: none !important;
- margin-bottom: 0px !important;
- clear: none !important;
- display: inline !important;
- font-size: 10px;
- line-height: 10px;
- height: 10px;
-}
-.nopad {
- margin-bottom: 0px !important;
- margin-top: 0px !important;
- padding-top: 0px !important;
- padding-bottom: 0px !important;
- padding: 0px 0px !important;
-}
-#album_table th#albumname,
-#upcoming_table th#artistname,
-#wanted_table th#artistname {
- min-width: 150px;
- text-align: center;
-}
-#album_table th#type,
-#track_table th#duration {
- min-width: 100px;
- text-align: center;
- width: 175px;
-}
-#album_table th#bitrate,
-#album_table th#albumformat {
- min-width: 60px;
- text-align: center;
-}
-#album_table td#select,
-#album_table td#albumart {
- text-align: left;
- vertical-align: middle;
-}
-#album_table td#albumname,
-#album_table td#reldate,
-#album_table td#type,
-#album_table td#score,
-#track_table td#duration,
-#upcoming_table td#select,
-#upcoming_table td#status,
-#wanted_table td#select,
-#wanted_table td#status {
- text-align: center;
- vertical-align: middle;
-}
-#album_table td#status,
-#album_table td#bitrate,
-#album_table td#score,
-#album_table td#albumformat,
-#album_table td#wantlossless {
- font-size: 13px;
- text-align: center;
- vertical-align: middle;
-}
div#albumheader .albuminfo li span,
div#artistheader h3 span {
font-weight: 400;
@@ -1500,16 +1505,10 @@ div#artistheader h3 span {
#track_table th#bitrate,
#track_table th#format,
#upcoming_table th#type,
-#wanted_table th#type,
-#searchresults_table th#score {
+#wanted_table th#type {
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;
@@ -1522,6 +1521,24 @@ div#artistheader h3 span {
text-align: center;
vertical-align: middle;
}
+#upcoming_table th#artistname,
+#wanted_table th#artistname {
+ min-width: 150px;
+ text-align: center;
+}
+#track_table th#duration {
+ min-width: 100px;
+ text-align: center;
+ width: 175px;
+}
+#track_table td#duration,
+#upcoming_table td#select,
+#upcoming_table td#status,
+#wanted_table td#select,
+#wanted_table td#status {
+ text-align: center;
+ vertical-align: middle;
+}
#upcoming_table td#albumart img,
#wanted_table td#albumart img {
background: #FFF;
@@ -1568,14 +1585,13 @@ div#artistheader h3 span {
vertical-align: middle;
}
#upcoming_table td#type,
-#wanted_table td#type,
-#searchresults_table td#score {
+#wanted_table td#type {
min-width: 75px;
text-align: center;
vertical-align: middle;
}
table tr td#status a {
- color: #4183c4;
+ color: #4183C4;
}
.ie7 input[type="checkbox"] {
vertical-align: baseline;
diff --git a/data/interfaces/default/css/style.less b/data/interfaces/default/css/style.less
index 1e164649..578310f5 100644
--- a/data/interfaces/default/css/style.less
+++ b/data/interfaces/default/css/style.less
@@ -26,6 +26,19 @@ body {
padding: 0;
}
+// Clearfix from http://stackoverflow.com/a/8554054/1115187
+.clearfix()
+{
+ &:after
+ {
+ content: " "; /* Older browser do not support empty content */
+ visibility: hidden;
+ display: block;
+ height: 0;
+ clear: both;
+ }
+}
+
// Links
a {
color: @link-color;
@@ -76,18 +89,149 @@ hr {
margin: 1em 0;
padding: 0;
}
-small {
- font-size: 85%;
+
+small, .small
+{
+ font-size: xx-small;
}
+
+// -------------------------------------------------
+// Config Page
+// -------------------------------------------------
+
+// OLD VERSION:
+.configtable {
+ legend {
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 10px;
+ text-shadow: 1px 1px 0 #FFFFFF;
+ }
+ tr {
+ td {
+ &:last-child {
+ border-left: 1px dotted #ddd;
+ padding-left: 20px;
+ }
+ }
+ }
+ td {
+ padding-right: 15px;
+ width: 50%;
+ }
+}
+
+// NEW VERSION:
+.config
+{
+ legend {
+ font-size: larger;
+ font-weight: bold;
+ margin-bottom: 1em;
+ text-shadow: 1px 1px 0 #FFFFFF;
+ }
+
+ .caption
+ {
+ color: @caption-color;
+ line-height: @base-font-size;
+ margin-top: 3px;
+ }
+
+ .tab
+ {
+ .columnRule( 1px solid @border-color );
+ .columnCount(2);
+ .columnGap(4em);
+
+ .message
+ {
+ .columnSpan(all);
+ display:block;
+ background-color:transparent;
+ margin-bottom:1em;
+ }
+ }
+
+ .block
+ {
+ -webkit-column-break-inside: avoid;
+ page-break-inside: avoid;
+ break-inside: avoid-column;
+ }
+
+ .option
+ {
+ // TODO : need responsive - remove floating, use flexbox
+ .clearfix();
+ margin-bottom: 1em;
+ font-size: @base-font-size;
+
+ .input-wrapper
+ {
+ // TODO : need responsive
+ float: left;
+ }
+
+ label
+ {
+ // TODO : need responsive
+ width: 170px;
+ font-size: @base-font-size;
+ // TODO : need responsive
+ float: left;
+ }
+
+ input
+ {
+ margin-right: 5px;
+ }
+
+ input[type=text],
+ input[type=password],
+ input[type=number],
+ {
+ border: 1px solid #DDD;
+ border-top: 1px solid #CDCDCD;
+ .shadow(inset 0 1px 1px #e0e0e0);
+ color: @text-color;
+ font-size: larger;
+ // TODO : need responsive
+ max-width: 230px;
+ padding: 3px 5px;
+ }
+
+ input[type=number]
+ {
+ max-width: 5em;
+ }
+ }
+
+ .embed-option-block
+ {
+ margin-left:1em;
+ }
+}
+
img {
- &.albumArt {
+ .-less-albumArt(@size)
+ {
float: left;
- min-height: 100%;
- min-width: 100%;
+ min-height: @size;
+ min-width: @size;
max-width: 250px;
max-height: 300px;
position: relative;
}
+
+ &.albumArt
+ {
+ .-less-albumArt(100%);
+ }
+ &.albumArt-nostretch
+ {
+ .-less-albumArt(64px);
+ }
}
.title {
margin-bottom:20px;
@@ -105,9 +249,9 @@ table {
border-collapse: collapse;
border-spacing: 0;
th {
- .gradient(#FAFAFA, #EAEAEA);
+ .gradient(@gradient-color-1, @gradient-color-2);
border-left: 1px solid #E0E0E0;
- .shadow(1px 0 0 #FAFAFA);
+ .shadow(1px 0 0 @gradient-color-1);
text-shadow:1px 1px 0 #FFFFFF ;
input[type="checkbox"]{ vertical-align: middle;}
&:first-child { border-left:0;.shadow(none)}
@@ -126,13 +270,13 @@ table {
// Forms
select, input, textarea, button
{
- font: 99%;
+ font-size: 99%;
}
-select
+select
{
- .rounded(5px);
- background: #4F4F4F;
+ .rounded(@heading-input-radius);
+ background: @brand-color;
border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
color: #fff;
@@ -155,7 +299,7 @@ button {
overflow: visible;
width: auto;
}
-input,select,form .checkbox input,.configtable td#middle, #artist_table td#have,#album_table td#have { vertical-align: middle; }
+input,select,form .checkbox input,.configtable td#middle, #artist_table td#have { vertical-align: middle; }
input[type="radio"]{ vertical-align: text-bottom; }
::-moz-selection, ::selection {
@@ -164,7 +308,7 @@ input[type="radio"]{ vertical-align: text-bottom; }
text-shadow: none;
}
input[type=submit], input[type=button] {
- .rounded(5px);
+ .rounded(@heading-input-radius);
background: #222222 url("../images/button.png") repeat-x;
border: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
@@ -197,8 +341,8 @@ form {
}
}
}
+
.row {
- font-family: Helvetica, Arial;
margin-bottom: 10px;
label {
display: block;
@@ -292,7 +436,7 @@ ul#nav {
float: right;
margin: 0;
padding: 0 0 0 10px;
- border-left: 1px solid #FAFAFA;
+ border-left: 1px solid @gradient-color-1;
.shadow(-1px 0 0 #e0e0e0);
height: 58px;
li {
@@ -314,8 +458,8 @@ ul#nav {
.gradient(#F1F1F1, #E0E0E0);
border: 1px solid #DDDDDD;
.rounded(3px);
- .shadow(0 1px 0 #FAFAFA);
- .shadow(0 1px 0 #FAFAFA inset);
+ .shadow(0 1px 0 @gradient-color-1);
+ .shadow(0 1px 0 @gradient-color-1 inset);
-webkit-transition:color .2s ease-in;
-moz-transition:color .2s ease-in;
-o-transition:color .2s ease-in;
@@ -339,8 +483,8 @@ ul#nav {
}
// Layout
header {
- .gradient(#fafafa, #eaeaea);
- border-bottom: 1px solid #CACACA;
+ .gradient(@gradient-color-1, @gradient-color-2);
+ border-bottom: 1px solid @border-color;
.shadow(0 0 10px rgba(0, 0, 0, 0.1));
height: 58px;
position: fixed;
@@ -350,7 +494,7 @@ header {
margin: 0 auto;
overflow: hidden;
position: relative;
- width: 960px;
+ width: 990px;
}
#logo {
float: left;
@@ -388,12 +532,12 @@ footer {
top: 4px;
}
}
-#ajaxMsg {
+.ajaxMsg {
border: 1px solid @border-color;
.gradient(#FFFFFF, #EEEEEE);
.rounded(7px);
display: none;
- font-size: 14px;
+ font-size: larger;
right: 10px;
.shadow(0px 0px 2px #aaa);
padding: 7px 10px;
@@ -433,9 +577,9 @@ footer {
}
#updatebar {
- #ajaxMsg;
+ .ajaxMsg;
display: block;
- .gradient(#FCF5C2,@msg-bg);
+ .gradient(#FCF5C2, @msg-bg);
}
// Subheader
@@ -450,29 +594,39 @@ footer {
list-style-type: none;
width: 100%;
z-index: 998;
- #subhead_menu {
+ #subhead_menu
+ {
float: right;
- margin-top: 5px;
+ margin-top: 0px;
position: relative;
z-index: 99;
- a {
- .gradient(#F4F4F4, #e7e7e7);
+ a
+ {
+ .gradient(@gradient-color-1, @gradient-color-2);
font-family: @base-font-face;
- font-size: 12px;
+ font-size: @base-font-size;
font-weight: normal;
- &:hover {
- .gradient(#599BDC, #3072B3);
- color: #FFF;
- border-color: #518CC6 #518CC6 #2A65A0;
+ &:hover
+ {
+ .gradient(#599BDC, #3072B3);
+ color: #FFF;
+ border-color: #518CC6 #518CC6 #2A65A0;
}
}
}
+
+ #back_to_previous_link
+ {
+ margin-top: 20px;
+ position: relative;
+ z-index: 99;
+ }
}
}
// Search
div#searchbar {
- border-left: 1px solid #FAFAFA;
+ border-left: 1px solid @gradient-color-1;
.shadow(-1px 0 0 #e0e0e0);
padding: 15px 0 15px 20px;
position: absolute;
@@ -481,16 +635,17 @@ div#searchbar {
input[type=text] {
border: 1px solid #DDD;
border-top: 1px solid #CDCDCD;
+ .rounded(@heading-input-radius);
.shadow(0 1px 0 #f1f1f1);
.shadow(inset 0 1px 1px #e0e0e0);
- color: #999;
+ color: @caption-color;
float: left;
- font-size: 14px;
+ font-size: larger;
height: auto;
line-height: normal;
margin-right: 5px;
padding: 4px 5px 4px 25px;
- width: 150px;
+ width: 185px;
}
.mini-icon {
color: #999;
@@ -501,28 +656,6 @@ div#searchbar {
}
}
-// Config Page
-.configtable {
- legend {
- font-size: 16px;
- font-weight: bold;
- margin-bottom: 10px;
- text-shadow: 1px 1px 0 #FFFFFF;
- }
- tr {
- td {
- &:last-child {
- border-left: 1px dotted #ddd;
- padding-left: 20px;
- }
- }
- }
- td {
- padding-right: 15px;
- width: 50%;
- }
-}
-
// TABLES
// wrappers
@@ -530,7 +663,7 @@ div#searchbar {
_height: 302px;
background-color: #FFF;
clear: both;
- margin: 30px auto 0;
+ margin: 60px auto 0;
min-height: 100px;
position: relative;
width: 100%;
@@ -744,6 +877,15 @@ div#artistheader {
}
}
+.nopad
+{
+ margin-bottom: 0 !important;
+ margin-top: 0 !important;
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ padding: 0 !important;
+}
+
#album_table {
background-color: #FFF;
th#select {
@@ -757,7 +899,8 @@ div#artistheader {
text-align: center;
width: 175px;
}
- th#status, th#albumart {
+ th#albumart
+ {
min-width: 50px;
text-align: left;
}
@@ -771,16 +914,65 @@ div#artistheader {
text-align: center;
width: 80px;
}
- td#albumart {
- img {
- background: #FFF;
- border: 1px solid #ccc;
- padding: 3px;
- }
+ th#albumname
+ {
+ min-width: 150px;
+ text-align: center;
}
- td#status {
- a#wantlossless {
- white-space: nowrap;
+ th#bitrate,
+ th#albumformat
+ {
+ min-width: 60px;
+ text-align: center;
+ }
+ th#type
+ {
+ min-width: 100px;
+ text-align: center;
+ width: 175px;
+ }
+
+ td
+ {
+ vertical-align: middle;
+ font-size: larger;
+
+ have
+ {
+ }
+
+ albumname,
+ reldate,
+ type,
+ bitrate,
+ albumformat,
+ score,
+ wantlossless
+ {
+ text-align: center;
+ }
+
+ select
+ {
+ text-align: left;
+ }
+
+ albumart
+ {
+ text-align: left;
+ img {
+ background: #FFF;
+ border: 1px solid #ccc;
+ padding: 3px;
+ }
+ }
+ status
+ {
+ text-align: center;
+ a#wantlossless
+ {
+ white-space: nowrap;
+ }
}
}
}
@@ -829,6 +1021,31 @@ div#artistheader {
}
}
+#downloads_table
+{
+ th
+ {
+ text-align: center;
+
+ title
+ {
+ min-width: 500px;
+ }
+ size
+ {
+ min-width: 80px;
+ }
+ provider
+ {
+ min-width: 100px;
+ }
+ kind
+ {
+ min-width: 80px;
+ }
+ }
+}
+
// History
#history_table {
background-color: #FFF;
@@ -874,18 +1091,111 @@ div#artistheader {
padding: 2px 10px;
}
}
+
// Searchresults
-#searchresults_table {
- th#albumname {
- min-width: 225px;
+#searchresults_table
+{
+ th
+ {
+ font-size: larger;
text-align: left;
+ min-width: 50px;
+
+ artistnamesmall
+ {
+ min-width: 125px;
+ }
+ reldate
+ {
+ min-width: 80px;
+ text-align: center;
+ }
+ format
+ {
+ }
+ tracks
+ {
+ }
+ albumname
+ {
+ min-width: 250px;
+ }
+ artistname
+ {
+ min-width: 325px;
+ }
+ score
+ {
+ min-width: 75px;
+ text-align: center;
+ }
+ scoresmall
+ {
+ }
}
- th#artistname {
- min-width: 325px;
+
+ td
+ {
+ vertical-align: middle;
text-align: left;
+ font-size: larger;
+ min-width: 50px;
+
+ artistnamesmall
+ {
+ min-width: 100px;
+ }
+ reldate
+ {
+ min-width: 80px;
+ text-align: center;
+ }
+ format
+ {
+ }
+ tracks
+ {
+ }
+
+ albumname
+ {
+ min-width: 220px;
+ }
+ artistname
+ {
+ min-width: 300px;
+ }
+ add
+ {
+ }
+
+ score
+ {
+ min-width: 75px;
+
+ .bar
+ {
+ width: 100px;
+ margin: 0 auto;
+ border: 1px solid @border-color;
+ padding: 1px;
+ background-color: #FFF;
+ .score
+ {
+ height: 14px;
+ color: @text-color;
+ color: #FFF;
+ font-size: 11px;
+ vertical-align: middle;
+ line-height: normal;
+ .gradient(#A3E532,#90CC2A);
+ }
+ }
+ }
}
+
#artistImg {
- background: url("../images/loader_black.gif") no-repeat scroll center center #FFFFFF;
+ background: url("../images/loader_black.gif") no-repeat scroll center center #FFFFFF;
border: 3px solid #FFFFFF;
box-shadow: 1px 1px 2px 0 #555555;
float: left;
@@ -894,36 +1204,8 @@ div#artistheader {
text-indent: -3000px;
width: 50px;
}
- td#albumname {
- min-width: 200px;
- text-align: left;
- vertical-align: middle;
- }
- td#artistname {
- min-width: 300px;
- text-align: left;
- vertical-align: middle;
- }
- td#add {vertical-align: middle;}
- td#score {
- .bar {
- width: 100px;
- margin: 0 auto;
- border: 1px solid @border-color;
- padding: 1px;
- background-color: #FFF;
- .score {
- height: 14px;
- color: @text-color;
- color: #FFF;
- font-size: 11px;
- vertical-align: middle;
- line-height: normal;
- .gradient(#A3E532,#90CC2A);
- }
- }
- }
}
+
.progress-container {
background: #FFF;
border: 1px solid #ccc;
@@ -997,6 +1279,12 @@ div#artistheader {
}
}
}
+
+#trashcan
+{
+ margin-top:15px;
+}
+
.cloudtag {
font-size: 16px;
#cloud {
@@ -1089,48 +1377,11 @@ div#artistheader {
}
.invisible { visibility: hidden; }
-.clearfix:before, .clearfix:after {
- content: "\0020";
- display: block;
- height: 0;
- overflow: hidden;
-}
-.clearfix {
- zoom: 1px;
- &:after { clear: both; }
-}
-
-// Table width
-#album_table th#albumname, #upcoming_table th#artistname, #wanted_table th#artistname {
- min-width: 150px;
- text-align: center;
-}
-#album_table th#type, #track_table th#duration {
- min-width: 100px;
- text-align: center;
- width: 175px;
-}
-#album_table th#bitrate, #album_table th#albumformat {
- min-width: 60px;
- text-align: center;
-}
-#album_table td#select, #album_table td#albumart {
- text-align: left;
- vertical-align: middle;
-}
-#album_table td#albumname,#album_table td#reldate, #album_table td#type, #track_table td#duration, #upcoming_table td#select, #upcoming_table td#status, #wanted_table td#select, #wanted_table td#status {
- text-align: center;
- vertical-align: middle;
-}
-#album_table td#status, #album_table td#bitrate, #album_table td#albumformat, #album_table td#wantlossless {
- font-size: 13px;
- text-align: center;
- vertical-align: middle;
-}
div#albumheader .albuminfo li span, div#artistheader h3 span {
font-weight: 400;
}
-#track_table th#bitrate, #track_table th#format, #upcoming_table th#type, #wanted_table th#type, #searchresults_table th#score {
+#track_table th#bitrate, #track_table th#format, #upcoming_table th#type, #wanted_table th#type
+{
min-width: 75px;
text-align: center;
}
@@ -1144,6 +1395,22 @@ div#albumheader .albuminfo li span, div#artistheader h3 span {
text-align: center;
vertical-align: middle;
}
+
+#upcoming_table th#artistname, #wanted_table th#artistname
+{
+ min-width: 150px;
+ text-align: center;
+}
+#track_table th#duration {
+ min-width: 100px;
+ text-align: center;
+ width: 175px;
+}
+#track_table td#duration, #upcoming_table td#select, #upcoming_table td#status, #wanted_table td#select, #wanted_table td#status {
+ text-align: center;
+ vertical-align: middle;
+}
+
#upcoming_table, #wanted_table {
td#albumart {
img {
@@ -1185,7 +1452,8 @@ div#albumheader .albuminfo li span, div#artistheader h3 span {
text-align: center;
vertical-align: middle;
}
-#upcoming_table td#type, #wanted_table td#type, #searchresults_table td#score {
+#upcoming_table td#type, #wanted_table td#type
+{
min-width: 75px;
text-align: center;
vertical-align: middle;
diff --git a/data/interfaces/default/fonts/FontAwesome.otf b/data/interfaces/default/fonts/FontAwesome.otf
new file mode 100644
index 00000000..59853bcd
Binary files /dev/null and b/data/interfaces/default/fonts/FontAwesome.otf differ
diff --git a/data/interfaces/default/fonts/fontawesome-webfont.eot b/data/interfaces/default/fonts/fontawesome-webfont.eot
old mode 100755
new mode 100644
index 7c79c6a6..96f92f9b
Binary files a/data/interfaces/default/fonts/fontawesome-webfont.eot and b/data/interfaces/default/fonts/fontawesome-webfont.eot differ
diff --git a/data/interfaces/default/fonts/fontawesome-webfont.svg b/data/interfaces/default/fonts/fontawesome-webfont.svg
old mode 100755
new mode 100644
index 45fdf338..5a5f0ecd
--- a/data/interfaces/default/fonts/fontawesome-webfont.svg
+++ b/data/interfaces/default/fonts/fontawesome-webfont.svg
@@ -14,10 +14,11 @@
+
-
+
-
+
@@ -30,7 +31,7 @@
-
+
@@ -52,7 +53,7 @@
-
+
@@ -77,11 +78,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -109,8 +110,8 @@
-
-
+
+
@@ -143,17 +144,17 @@
-
-
+
+
-
+
-
+
-
+
@@ -168,7 +169,7 @@
-
+
@@ -176,14 +177,14 @@
-
-
+
+
-
+
@@ -218,8 +219,8 @@
-
-
+
+
@@ -247,10 +248,10 @@
-
+
-
+
@@ -274,7 +275,7 @@
-
+
@@ -345,8 +346,8 @@
-
-
+
+
@@ -361,14 +362,14 @@
-
+
-
-
+
+
@@ -379,7 +380,7 @@
-
+
@@ -398,17 +399,287 @@
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data/interfaces/default/fonts/fontawesome-webfont.ttf b/data/interfaces/default/fonts/fontawesome-webfont.ttf
old mode 100755
new mode 100644
index e89738de..86784df9
Binary files a/data/interfaces/default/fonts/fontawesome-webfont.ttf and b/data/interfaces/default/fonts/fontawesome-webfont.ttf differ
diff --git a/data/interfaces/default/fonts/fontawesome-webfont.woff b/data/interfaces/default/fonts/fontawesome-webfont.woff
old mode 100755
new mode 100644
index 8c1748aa..c7faa19c
Binary files a/data/interfaces/default/fonts/fontawesome-webfont.woff and b/data/interfaces/default/fonts/fontawesome-webfont.woff differ
diff --git a/data/interfaces/default/fonts/fontawesome-webfont.woff2 b/data/interfaces/default/fonts/fontawesome-webfont.woff2
new file mode 100644
index 00000000..cab8571d
Binary files /dev/null and b/data/interfaces/default/fonts/fontawesome-webfont.woff2 differ
diff --git a/headphones/__init__.py b/headphones/__init__.py
index 4b36acf7..1cb8979b 100644
--- a/headphones/__init__.py
+++ b/headphones/__init__.py
@@ -87,7 +87,7 @@ LATEST_VERSION = None
COMMITS_BEHIND = None
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma"]
-LOSSLESS_MEDIA_FORMATS = ["flac"]
+LOSSLESS_MEDIA_FORMATS = ["flac", "aiff"]
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
@@ -132,7 +132,7 @@ def initialize(config_file):
CONFIG.LOG_DIR = None
if not QUIET:
- sys.stderr.write("Unable to create the log directory. " \
+ sys.stderr.write("Unable to create the log directory. "
"Logging to screen only.\n")
# Start the logger, disable console if needed
@@ -143,7 +143,7 @@ def initialize(config_file):
SOFT_CHROOT = SoftChroot(str(CONFIG.SOFT_CHROOT))
if SOFT_CHROOT.isEnabled():
logger.info("Soft-chroot enabled for dir: %s", str(CONFIG.SOFT_CHROOT))
- except exceptions.SoftChrootError as e:
+ except headphones.exceptions.SoftChrootError as e:
logger.error("SoftChroot error: %s", e)
raise e
diff --git a/headphones/albumart_test.py b/headphones/albumart_test.py
index e1b80c02..948b70f2 100644
--- a/headphones/albumart_test.py
+++ b/headphones/albumart_test.py
@@ -1,11 +1,8 @@
-#import unittest
-#import mock
from headphones.unittestcompat import TestCase
import headphones.albumart
-# no tests...
class AlbumArtTest(TestCase):
def test_nothing(self):
x = 100 - 2 * 50
diff --git a/headphones/albumswitcher.py b/headphones/albumswitcher.py
index d4d05aa1..74f1077f 100644
--- a/headphones/albumswitcher.py
+++ b/headphones/albumswitcher.py
@@ -79,7 +79,7 @@ def switch(AlbumID, ReleaseID):
'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)):
+ headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
myDB.action(
'UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID])
diff --git a/headphones/cache.py b/headphones/cache.py
index 219be7fb..202b7802 100644
--- a/headphones/cache.py
+++ b/headphones/cache.py
@@ -421,7 +421,7 @@ class Cache(object):
# 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])):
+ self.thumb_files and self._is_current(self.thumb_files[0])):
artwork = request.request_content(thumb_url, timeout=20)
if artwork:
diff --git a/headphones/config.py b/headphones/config.py
index dd1ae9b6..5b819007 100644
--- a/headphones/config.py
+++ b/headphones/config.py
@@ -55,7 +55,7 @@ _CONFIG_DEFINITIONS = {
'CHECK_GITHUB_INTERVAL': (int, 'General', 360),
'CHECK_GITHUB_ON_STARTUP': (int, 'General', 1),
'CLEANUP_FILES': (int, 'General', 0),
- 'CONFIG_VERSION': (str, 'General', '0'),
+ 'CONFIG_VERSION': (str, 'General', '2'),
'CORRECT_METADATA': (int, 'General', 0),
'CUE_SPLIT': (int, 'General', 1),
'CUE_SPLIT_FLAC_PATH': (path, 'General', ''),
@@ -68,6 +68,7 @@ _CONFIG_DEFINITIONS = {
'CUSTOMUSER': (str, 'General', ''),
'DELETE_LOSSLESS_FILES': (int, 'General', 1),
'DELUGE_HOST': (str, 'Deluge', ''),
+ 'DELUGE_CERT': (str, 'Deluge', ''),
'DELUGE_PASSWORD': (str, 'Deluge', ''),
'DELUGE_LABEL': (str, 'Deluge', ''),
'DELUGE_DONE_DIRECTORY': (str, 'Deluge', ''),
@@ -104,11 +105,11 @@ _CONFIG_DEFINITIONS = {
'EXTRAS': (str, 'General', ''),
'EXTRA_NEWZNABS': (list, 'Newznab', ''),
'EXTRA_TORZNABS': (list, 'Torznab', ''),
- 'FILE_FORMAT': (str, 'General', 'Track Artist - Album [Year] - Title'),
+ 'FILE_FORMAT': (str, 'General', '$Track $Artist - $Album [$Year] - $Title'),
'FILE_PERMISSIONS': (str, 'General', '0644'),
'FILE_PERMISSIONS_ENABLED': (bool_int, 'General', True),
'FILE_UNDERSCORES': (int, 'General', 0),
- 'FOLDER_FORMAT': (str, 'General', 'Artist/Album [Year]'),
+ 'FOLDER_FORMAT': (str, 'General', '$Artist/$Album [$Year]'),
'FOLDER_PERMISSIONS_ENABLED': (bool_int, 'General', True),
'FOLDER_PERMISSIONS': (str, 'General', '0755'),
'FREEZE_DB': (int, 'General', 0),
@@ -441,59 +442,9 @@ class Config(object):
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'
-
+ """
+ Bring old configs up to date
+ """
if self.CONFIG_VERSION == '2':
# Update the config to use direct path to the encoder rather than the encoder folder
if self.ENCODERFOLDER:
diff --git a/headphones/config_test.py b/headphones/config_test.py
index 1cd5367f..77fda697 100644
--- a/headphones/config_test.py
+++ b/headphones/config_test.py
@@ -283,7 +283,7 @@ class ConfigApiTest(TestCase):
""" Config: get_extra_newznabs """
path = '/tmp/notexist'
- #itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
+ # itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
# set up mocks:
# 'EXTRA_NEWZNABS': (list, 'Newznab', ''),
# 'EXTRA_TORZNABS': (list, 'Torznab', ''),
@@ -372,7 +372,7 @@ class ConfigApiTest(TestCase):
""" Config: get_extra_torznabs """
path = '/tmp/notexist'
- #itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
+ # itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
# set up mocks:
# 'EXTRA_TORZNABS': (list, '', ''),
self.config_mock["Torznab"] = {"extra_torznabs": conf_value}
diff --git a/headphones/deluge.py b/headphones/deluge.py
index 5f79d29e..6809dd45 100644
--- a/headphones/deluge.py
+++ b/headphones/deluge.py
@@ -35,7 +35,6 @@
from __future__ import unicode_literals
from headphones import logger
-#from headphones import request
import time
import re
@@ -48,22 +47,63 @@ import traceback
delugeweb_auth = {}
delugeweb_url = ''
+deluge_verify_cert = False
+scrub_logs = True
-def addTorrent(link, data=None):
+def _scrubber(text):
+ if scrub_logs:
+ try:
+ # URL parameter values
+ text = re.sub('=[0-9a-zA-Z]*', '=REMOVED', text)
+ # Local host with port
+ # text = re.sub('\:\/\/.*\:', '://REMOVED:', text) # just host
+ text = re.sub('\:\/\/.*\:[0-9]*', '://REMOVED:', text)
+ # Session cookie
+ text = re.sub("_session_id'\: '.*'", "_session_id': 'REMOVED'", text)
+ # Local Windows user path
+ if text.lower().startswith('c:\\users\\'):
+ k = text.split('\\')
+ text = '\\'.join([k[0], k[1], '.....', k[-1]])
+ # partial_link = re.sub('(auth.*?)=.*&','\g<1>=SECRETZ&', link)
+ # partial_link = re.sub('(\w)=[0-9a-zA-Z]*&*','\g<1>=REMOVED&', link)
+ except Exception as e:
+ logger.debug('Deluge: Scrubber failed: %s' % str(e))
+ return text
+
+
+def addTorrent(link, data=None, name=None):
try:
+ # Authenticate anyway
+ logger.debug('Deluge: addTorrent Authentication')
+ _get_auth()
+
result = {}
retid = False
+ url_what = ['https://what.cd/', 'http://what.cd/']
+ url_waffles = ['https://waffles.fm/', 'http://waffles.fm/']
- if link.startswith('magnet:'):
- logger.debug('Deluge: Got a magnet link: %s' % link)
+ if link.lower().startswith('magnet:'):
+ logger.debug('Deluge: Got a magnet link: %s' % _scrubber(link))
result = {'type': 'magnet',
'url': link}
retid = _add_torrent_magnet(result)
- elif link.startswith('http://') or link.startswith('https://'):
- logger.debug('Deluge: Got a URL: %s' % link)
- user_agent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
+ elif link.lower().startswith('http://') or link.lower().startswith('https://'):
+ logger.debug('Deluge: Got a URL: %s' % _scrubber(link))
+ if link.lower().startswith(tuple(url_waffles)):
+ if 'rss=' not in link:
+ link = link + '&rss=1'
+ if link.lower().startswith(tuple(url_what)):
+ logger.debug('Deluge: Using different User-Agent for this site')
+ user_agent = 'Headphones'
+ # This method will make Deluge download the file
+ # logger.debug('Deluge: Letting Deluge download this')
+ # local_torrent_path = _add_torrent_url({'url': link})
+ # logger.debug('Deluge: Returned this local path: %s' % _scrubber(local_torrent_path))
+ # return addTorrent(local_torrent_path)
+ else:
+ user_agent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
headers = {'User-Agent': user_agent}
torrentfile = ''
logger.debug('Deluge: Trying to download (GET)')
@@ -71,39 +111,38 @@ def addTorrent(link, data=None):
r = requests.get(link, headers=headers)
if r.status_code == 200:
logger.debug('Deluge: 200 OK')
- torrentfile = r.text
- #for chunk in r.iter_content(chunk_size=1024):
- # if chunk: # filter out keep-alive new chunks
- # torrentfile = torrentfile + chunk
+ # .text will ruin the encoding for some torrents
+ torrentfile = r.content
else:
- logger.debug('Deluge: Trying to GET %s returned status %d' % (link, r.status_code))
+ logger.debug('Deluge: Trying to GET %s returned status %d' % (_scrubber(link), r.status_code))
return False
except Exception as e:
logger.debug('Deluge: Download failed: %s' % str(e))
- if 'announce' not in torrentfile[:40]:
- logger.debug('Deluge: Contents of %s doesn\'t look like a torrent file' % link)
+ if 'announce' not in str(torrentfile)[:40]:
+ logger.debug('Deluge: Contents of %s doesn\'t look like a torrent file' % _scrubber(link))
return False
- # Extract torrent name from .torrent
- try:
- logger.debug('Deluge: Getting torrent name length')
- name_length = int(re.findall('name([0-9]*)\:.*?\:', torrentfile)[0])
- logger.debug('Deluge: Getting torrent name')
- name = re.findall('name[0-9]*\:(.*?)\:', torrentfile)[0][:name_length]
- except Exception as e:
- logger.debug('Deluge: Could not get torrent name, getting file name')
- # get last part of link/path (name only)
- name = link.split('\\')[-1].split('/')[-1]
- # remove '.torrent' suffix
- if name[-len('.torrent'):] == '.torrent':
- name = name[:-len('.torrent')]
- logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, torrentfile[:40]))
+ if not name:
+ # Extract torrent name from .torrent
+ try:
+ logger.debug('Deluge: Getting torrent name length')
+ name_length = int(re.findall('name([0-9]*)\:.*?\:', str(torrentfile))[0])
+ logger.debug('Deluge: Getting torrent name')
+ name = re.findall('name[0-9]*\:(.*?)\:', str(torrentfile))[0][:name_length]
+ except Exception as e:
+ logger.debug('Deluge: Could not get torrent name, getting file name')
+ # get last part of link/path (name only)
+ name = link.split('\\')[-1].split('/')[-1]
+ # remove '.torrent' suffix
+ if name[-len('.torrent'):] == '.torrent':
+ name = name[:-len('.torrent')]
+ logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
result = {'type': 'torrent',
'name': name,
'content': torrentfile}
retid = _add_torrent_file(result)
# elif link.endswith('.torrent') or data:
- elif not (link.startswith('http://') or link.startswith('https://')):
+ elif not (link.lower().startswith('http://') or link.lower().startswith('https://')):
if data:
logger.debug('Deluge: Getting .torrent data')
torrentfile = data
@@ -111,20 +150,21 @@ def addTorrent(link, data=None):
logger.debug('Deluge: Getting .torrent file')
with open(link, 'rb') as f:
torrentfile = f.read()
- # Extract torrent name from .torrent
- try:
- logger.debug('Deluge: Getting torrent name length')
- name_length = int(re.findall('name([0-9]*)\:.*?\:', torrentfile)[0])
- logger.debug('Deluge: Getting torrent name')
- name = re.findall('name[0-9]*\:(.*?)\:', torrentfile)[0][:name_length]
- except Exception as e:
- logger.debug('Deluge: Could not get torrent name, getting file name')
- # get last part of link/path (name only)
- name = link.split('\\')[-1].split('/')[-1]
- # remove '.torrent' suffix
- if name[-len('.torrent'):] == '.torrent':
- name = name[:-len('.torrent')]
- logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, torrentfile[:40]))
+ if not name:
+ # Extract torrent name from .torrent
+ try:
+ logger.debug('Deluge: Getting torrent name length')
+ name_length = int(re.findall('name([0-9]*)\:.*?\:', str(torrentfile))[0])
+ logger.debug('Deluge: Getting torrent name')
+ name = re.findall('name[0-9]*\:(.*?)\:', str(torrentfile))[0][:name_length]
+ except Exception as e:
+ logger.debug('Deluge: Could not get torrent name, getting file name')
+ # get last part of link/path (name only)
+ name = link.split('\\')[-1].split('/')[-1]
+ # remove '.torrent' suffix
+ if name[-len('.torrent'):] == '.torrent':
+ name = name[:-len('.torrent')]
+ logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
result = {'type': 'torrent',
'name': name,
'content': torrentfile}
@@ -137,7 +177,7 @@ def addTorrent(link, data=None):
logger.info('Deluge: Torrent sent to Deluge successfully (%s)' % retid)
return retid
else:
- logger.info('Deluge returned status %s' % retid)
+ logger.info('Deluge: Returned status %s' % retid)
return False
except Exception as e:
@@ -157,15 +197,17 @@ def getTorrentFolder(result):
result['hash'],
["total_done"]
],
- "id": 22})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ "id": 21})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
result['total_done'] = json.loads(response.text)['result']['total_done']
tries = 0
while result['total_done'] == 0 and tries < 10:
tries += 1
time.sleep(5)
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
result['total_done'] = json.loads(response.text)['result']['total_done']
post_data = json.dumps({"method": "web.get_torrent_status",
@@ -183,7 +225,8 @@ def getTorrentFolder(result):
],
"id": 23})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
result['save_path'] = json.loads(response.text)['result']['save_path']
result['name'] = json.loads(response.text)['result']['name']
@@ -194,34 +237,51 @@ def getTorrentFolder(result):
def removeTorrent(torrentid, remove_data=False):
-
+ logger.debug('Deluge: Remove torrent %s' % torrentid)
if not any(delugeweb_auth):
_get_auth()
- result = False
- post_data = json.dumps({"method": "core.remove_torrent",
- "params": [
- torrentid,
- remove_data
- ],
- "id": 25})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
- result = json.loads(response.text)['result']
+ try:
+ result = False
+ post_data = json.dumps({"method": "core.remove_torrent",
+ "params": [
+ torrentid,
+ remove_data
+ ],
+ "id": 25})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
+ result = json.loads(response.text)['result']
- return result
+ return result
+ except Exception as e:
+ logger.error('Deluge: Removing torrent failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
+ return None
def _get_auth():
logger.debug('Deluge: Authenticating...')
- global delugeweb_auth, delugeweb_url
+ global delugeweb_auth, delugeweb_url, deluge_verify_cert
delugeweb_auth = {}
delugeweb_host = headphones.CONFIG.DELUGE_HOST
+ delugeweb_cert = headphones.CONFIG.DELUGE_CERT
delugeweb_password = headphones.CONFIG.DELUGE_PASSWORD
+ logger.debug('Deluge: Using password %s******%s' % (delugeweb_password[0], delugeweb_password[-1]))
if not delugeweb_host.startswith('http'):
delugeweb_host = 'http://%s' % delugeweb_host
+ if delugeweb_cert is None or delugeweb_cert.strip() == '':
+ deluge_verify_cert = False
+ logger.debug('Deluge: FYI no SSL certificate configured')
+ else:
+ deluge_verify_cert = delugeweb_cert
+ delugeweb_host = delugeweb_host.replace('http:', 'https:')
+ logger.debug('Deluge: Using certificate %s, host is now %s' % (_scrubber(deluge_verify_cert), _scrubber(delugeweb_host)))
+
if delugeweb_host.endswith('/'):
delugeweb_host = delugeweb_host[:-1]
@@ -231,33 +291,59 @@ def _get_auth():
"params": [delugeweb_password],
"id": 1})
try:
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
- # , verify=TORRENT_VERIFY_CERT)
- except Exception:
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
+ except requests.ConnectionError:
+ try:
+ logger.debug('Deluge: Connection failed, let\'s try HTTPS just in case')
+ response = requests.post(delugeweb_url.replace('http:', 'https:'), data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
+ # If the previous line didn't fail, change delugeweb_url for the rest of this session
+ logger.error('Deluge: Switching to HTTPS, but certificate won\'t be verified because NO CERTIFICATE WAS CONFIGURED!')
+ delugeweb_url = delugeweb_url.replace('http:', 'https:')
+ except Exception as e:
+ logger.error('Deluge: Authentication failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
+ return None
+ except Exception as e:
+ logger.error('Deluge: Authentication failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
return None
auth = json.loads(response.text)["result"]
+ auth_error = json.loads(response.text)["error"]
+ logger.debug('Deluge: Authentication result: %s, Error: %s' % (auth, auth_error))
delugeweb_auth = response.cookies
-
+ logger.debug('Deluge: Authentication cookies: %s' % _scrubber(str(delugeweb_auth.get_dict())))
post_data = json.dumps({"method": "web.connected",
"params": [],
"id": 10})
try:
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
- # , verify=TORRENT_VERIFY_CERT)
- except Exception:
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
+ except Exception as e:
+ logger.error('Deluge: Authentication failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
return None
connected = json.loads(response.text)['result']
+ connected_error = json.loads(response.text)['error']
+ logger.debug('Deluge: Connection result: %s, Error: %s' % (connected, connected_error))
if not connected:
post_data = json.dumps({"method": "web.get_hosts",
"params": [],
"id": 11})
try:
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
- # , verify=TORRENT_VERIFY_CERT)
- except Exception:
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
+ except Exception as e:
+ logger.error('Deluge: Authentication failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
return None
delugeweb_hosts = json.loads(response.text)['result']
@@ -270,9 +356,12 @@ def _get_auth():
"id": 11})
try:
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
- # , verify=TORRENT_VERIFY_CERT)
- except Exception:
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
+ except Exception as e:
+ logger.error('Deluge: Authentication failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
return None
post_data = json.dumps({"method": "web.connected",
@@ -280,9 +369,12 @@ def _get_auth():
"id": 10})
try:
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
- # , verify=TORRENT_VERIFY_CERT)
- except Exception:
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
+ except Exception as e:
+ logger.error('Deluge: Authentication failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
return None
connected = json.loads(response.text)['result']
@@ -302,14 +394,18 @@ def _add_torrent_magnet(result):
post_data = json.dumps({"method": "core.add_torrent_magnet",
"params": [result['url'], {}],
"id": 2})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
result['hash'] = json.loads(response.text)['result']
- logger.debug('Deluge: Response was %s' % str(json.loads(response.text)['result']))
+ logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result']
except Exception as e:
logger.error('Deluge: Adding torrent magnet failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
+ return False
+
-'''
def _add_torrent_url(result):
logger.debug('Deluge: Adding URL')
if not any(delugeweb_auth):
@@ -317,14 +413,17 @@ def _add_torrent_url(result):
try:
post_data = json.dumps({"method": "web.download_torrent_from_url",
"params": [result['url'], {}],
- "id": 2})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
- result['hash'] = json.loads(response.text)['result']
- logger.debug('Deluge: Response was %s' % str(json.loads(response.text)['result']))
+ "id": 32})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
+ result['location'] = json.loads(response.text)['result']
+ logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result']
except Exception as e:
logger.error('Deluge: Adding torrent URL failed: %s' % str(e))
-'''
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
+ return False
def _add_torrent_file(result):
@@ -336,14 +435,33 @@ def _add_torrent_file(result):
post_data = json.dumps({"method": "core.add_torrent_file",
"params": [result['name'] + '.torrent', b64encode(result['content'].encode('utf8')), {}],
"id": 2})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
result['hash'] = json.loads(response.text)['result']
- logger.debug('Deluge: Response was %s' % str(json.loads(response.text)['result']))
+ logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
return json.loads(response.text)['result']
+ except UnicodeDecodeError:
+ try:
+ # content is torrent file contents that needs to be encoded to base64
+ # this time let's try leaving the encoding as is
+ post_data = json.dumps({"method": "core.add_torrent_file",
+ "params": [result['name'] + '.torrent', b64encode(result['content']), {}],
+ "id": 22})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
+ result['hash'] = json.loads(response.text)['result']
+ logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
+ return json.loads(response.text)['result']
+ except Exception as e:
+ logger.error('Deluge: Adding torrent file failed after decode: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
+ return False
except Exception as e:
logger.error('Deluge: Adding torrent file failed: %s' % str(e))
formatted_lines = traceback.format_exc().splitlines()
logger.error('; '.join(formatted_lines))
+ return False
def setTorrentLabel(result):
@@ -361,7 +479,8 @@ def setTorrentLabel(result):
post_data = json.dumps({"method": 'label.get_labels',
"params": [],
"id": 3})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
labels = json.loads(response.text)['result']
if labels is not None:
@@ -371,7 +490,8 @@ def setTorrentLabel(result):
post_data = json.dumps({"method": 'label.add',
"params": [label],
"id": 4})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
logger.debug('Deluge: %s label added to Deluge' % label)
except Exception as e:
logger.error('Deluge: Setting label failed: %s' % str(e))
@@ -382,7 +502,8 @@ def setTorrentLabel(result):
post_data = json.dumps({"method": 'label.set_torrent',
"params": [result['hash'], label],
"id": 5})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
logger.debug('Deluge: %s label added to torrent' % label)
else:
logger.debug('Deluge: Label plugin not detected')
@@ -400,19 +521,27 @@ def setSeedRatio(result):
if result['ratio']:
ratio = result['ratio']
- if ratio:
- post_data = json.dumps({"method": "core.set_torrent_stop_at_ratio",
- "params": [result['hash'], True],
- "id": 5})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
- post_data = json.dumps({"method": "core.set_torrent_stop_ratio",
- "params": [result['hash'], float(ratio)],
- "id": 6})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ try:
+ if ratio:
+ post_data = json.dumps({"method": "core.set_torrent_stop_at_ratio",
+ "params": [result['hash'], True],
+ "id": 5})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
+ post_data = json.dumps({"method": "core.set_torrent_stop_ratio",
+ "params": [result['hash'], float(ratio)],
+ "id": 6})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
- return not json.loads(response.text)['error']
+ return not json.loads(response.text)['error']
- return True
+ return True
+ except Exception as e:
+ logger.error('Deluge: Setting seed ratio failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
+ return None
def setTorrentPath(result):
@@ -420,28 +549,36 @@ def setTorrentPath(result):
if not any(delugeweb_auth):
_get_auth()
- if headphones.CONFIG.DELUGE_DONE_DIRECTORY or headphones.CONFIG.DOWNLOAD_TORRENT_DIR:
- post_data = json.dumps({"method": "core.set_torrent_move_completed",
- "params": [result['hash'], True],
- "id": 7})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ try:
+ if headphones.CONFIG.DELUGE_DONE_DIRECTORY or headphones.CONFIG.DOWNLOAD_TORRENT_DIR:
+ post_data = json.dumps({"method": "core.set_torrent_move_completed",
+ "params": [result['hash'], True],
+ "id": 7})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
- if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
- move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
- else:
- move_to = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
+ if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
+ move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
+ else:
+ move_to = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
- if not os.path.exists(move_to):
- logger.debug('Deluge: %s directory doesn\'t exist, let\'s create it' % move_to)
- os.makedirs(move_to)
- post_data = json.dumps({"method": "core.set_torrent_move_completed_path",
- "params": [result['hash'], move_to],
- "id": 8})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ if not os.path.exists(move_to):
+ logger.debug('Deluge: %s directory doesn\'t exist, let\'s create it' % move_to)
+ os.makedirs(move_to)
+ post_data = json.dumps({"method": "core.set_torrent_move_completed_path",
+ "params": [result['hash'], move_to],
+ "id": 8})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
- return not json.loads(response.text)['error']
+ return not json.loads(response.text)['error']
- return True
+ return True
+ except Exception as e:
+ logger.error('Deluge: Setting torrent move-to directory failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
+ return None
def setTorrentPause(result):
@@ -449,12 +586,19 @@ def setTorrentPause(result):
if not any(delugeweb_auth):
_get_auth()
- if headphones.CONFIG.DELUGE_PAUSED:
- post_data = json.dumps({"method": "core.pause_torrent",
- "params": [[result['hash']]],
- "id": 9})
- response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth)
+ try:
+ if headphones.CONFIG.DELUGE_PAUSED:
+ post_data = json.dumps({"method": "core.pause_torrent",
+ "params": [[result['hash']]],
+ "id": 9})
+ response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
+ verify=deluge_verify_cert)
- return not json.loads(response.text)['error']
+ return not json.loads(response.text)['error']
- return True
+ return True
+ except Exception as e:
+ logger.error('Deluge: Setting torrent paused failed: %s' % str(e))
+ formatted_lines = traceback.format_exc().splitlines()
+ logger.error('; '.join(formatted_lines))
+ return None
diff --git a/headphones/helpers.py b/headphones/helpers.py
index 5e836178..ff062d57 100644
--- a/headphones/helpers.py
+++ b/headphones/helpers.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# This file is part of Headphones.
#
# Headphones is free software: you can redistribute it and/or modify
@@ -198,13 +199,15 @@ def replace_all(text, dic, normalize=False):
if normalize:
new_dic = {}
for i, j in dic.iteritems():
- try:
- if sys.platform == 'darwin':
- j = unicodedata.normalize('NFD', j)
- else:
- j = unicodedata.normalize('NFC', j)
- except TypeError:
- j = unicodedata.normalize('NFC', j.decode(headphones.SYS_ENCODING, 'replace'))
+ if j is not None:
+ try:
+ if sys.platform == 'darwin':
+ j = unicodedata.normalize('NFD', j)
+ else:
+ j = unicodedata.normalize('NFC', j)
+ except TypeError:
+ j = unicodedata.normalize('NFC',
+ j.decode(headphones.SYS_ENCODING, 'replace'))
new_dic[i] = j
dic = new_dic
return pathrender.render(text, dic)[0]
@@ -219,12 +222,104 @@ def replace_illegal_chars(string, type="file"):
return string
-def cleanName(string):
- pass1 = latinToAscii(string).lower()
- out_string = re.sub('[\.\-\/\!\@\#\$\%\^\&\*\(\)\+\-\"\'\,\;\:\[\]\{\}\<\>\=\_]', '',
- pass1).encode('utf-8')
+_CN_RE1 = re.compile(ur'[^\w]+', re.UNICODE)
+_CN_RE2 = re.compile(ur'[\s_]+', re.UNICODE)
- return out_string
+
+_XLATE_GRAPHICAL_AND_DIACRITICAL = {
+ # Translation table.
+ # Covers the following letters, for which NFD fails because of lack of
+ # combining character:
+ # ©ª«®²³¹»¼½¾ÆÐØÞßæðøþĐđĦħıIJijĸĿŀŁłŒœŦŧDŽDždžLJLjljNJNjnjǤǥDZDzdzȤȥ. This
+ # includes also some graphical symbols which can be easily replaced and
+ # usually are written by people who don't have appropriate keyboard layout.
+ u'©': '(C)', u'ª': 'a.', u'«': '<<', u'®': '(R)', u'²': '2', u'³': '3',
+ u'¹': '1', u'»': '>>', u'¼': ' 1/4 ', u'½': ' 1/2 ', u'¾': ' 3/4 ',
+ u'Æ': 'AE', u'Ð': 'D', u'Ø': 'O', u'Þ': 'Th', u'ß': 'ss', u'æ': 'ae',
+ u'ð': 'd', u'ø': 'o', u'þ': 'th', u'Đ': 'D', u'đ': 'd', u'Ħ': 'H',
+ u'ħ': 'h', u'ı': 'i', u'IJ': 'IJ', u'ij': 'ij', u'ĸ': 'q', u'Ŀ': 'L',
+ u'ŀ': 'l', u'Ł': 'L', u'ł': 'l', u'Œ': 'OE', u'œ': 'oe', u'Ŧ': 'T',
+ u'ŧ': 't', u'DŽ': 'DZ', u'Dž': 'Dz', u'LJ': 'LJ', u'Lj': 'Lj',
+ u'lj': 'lj', u'NJ': 'NJ', u'Nj': 'Nj', u'nj': 'nj',
+ u'Ǥ': 'G', u'ǥ': 'g', u'DZ': 'DZ', u'Dz': 'Dz', u'dz': 'dz',
+ u'Ȥ': 'Z', u'ȥ': 'z', u'№': 'No.',
+ u'º': 'o.', # normalize Nº abbrev (popular w/ classical music),
+ # this is 'masculine ordering indicator', not degree
+}
+
+_XLATE_SPECIAL = {
+ # Translation table.
+ # Cover additional special characters processing normalization.
+ u"'": '', # replace apostrophe with nothing
+ u'&': ' and ', # expand & to ' and '
+}
+
+
+def _translate(s, dictionary):
+ # type: (basestring,Mapping[basestring,basestring])->basestring
+ return ''.join(dictionary.get(x, x) for x in s)
+
+
+_COMBINING_RANGES = (
+ (0x0300, 0x036f), # Combining Diacritical Marks
+ (0x1ab0, 0x1aff), # Combining Diacritical Marks Extended
+ (0x20d0, 0x20ff), # Combining Diacritical Marks for Symbols
+ (0x1dc0, 0x1dff) # Combining Diacritical Marks Supplement
+)
+
+
+def _is_unicode_combining(u):
+ # type: (unicode)->bool
+ """
+ Check if input unicode is combining diacritical mark.
+ """
+ i = ord(u)
+ for r in _COMBINING_RANGES:
+ if r[0] <= i <= r[1]:
+ return True
+ return False
+
+
+def _transliterate(u, xlate):
+ # type: (unicode)->unicode
+ """
+ Perform transliteration using the specified dictionary
+ """
+ u = unicodedata.normalize('NFD', u)
+ u = u''.join([u'' if _is_unicode_combining(x) else x for x in u])
+ u = _translate(u, xlate)
+ # at this point output is either unicode, or plain ascii
+ return unicode(u)
+
+
+def clean_name(s):
+ # type: (basestring)->unicode
+ """Remove non-alphanumeric characters from the string, perform
+ normalization and substitution of some special characters; coalesce spaces.
+ :param s: string to clean up, possibly unicode one.
+ :return: cleaned-up version of input string.
+ """
+ if not isinstance(s, unicode):
+ # ignore extended chars if someone was dumb enough to pass non-ascii
+ # narrow string here, use only unicode for meaningful texts
+ u = unicode(s, 'ascii', 'replace')
+ else:
+ u = s
+ # 1. don't bother doing normalization NFKC, rather transliterate
+ # using special translation table
+ u = _transliterate(u, _XLATE_GRAPHICAL_AND_DIACRITICAL)
+ # 2. normalize NFKC the result
+ u = unicodedata.normalize('NFKC', u)
+ # 3. translate spacials
+ u = _translate(u, _XLATE_SPECIAL)
+ # 4. replace any non-alphanumeric character sequences by spaces
+ u = _CN_RE1.sub(u' ', u)
+ # 5. coalesce interleaved space/underscore sequences
+ u = _CN_RE2.sub(u' ', u)
+ # 6. trim
+ u = u.strip()
+ # 7. lowercase
+ return u
def cleanTitle(title):
@@ -538,8 +633,8 @@ def preserve_torrent_directory(albumpath):
shutil.copytree(albumpath, new_folder)
return new_folder
except Exception as e:
- logger.warn("Cannot copy/move files to temp folder: " + \
- new_folder.decode(headphones.SYS_ENCODING, 'replace') + \
+ logger.warn("Cannot copy/move files to temp folder: " +
+ new_folder.decode(headphones.SYS_ENCODING, 'replace') +
". Not continuing. Error: " + str(e))
return None
@@ -684,7 +779,7 @@ def walk_directory(basedir, followlinks=True):
real_path = os.path.abspath(os.readlink(path))
if real_path in traversed:
- logger.debug("Skipping '%s' since it is a symlink to " \
+ logger.debug("Skipping '%s' since it is a symlink to "
"'%s', which is already visited.", path, real_path)
else:
traversed.append(real_path)
diff --git a/headphones/helpers_test.py b/headphones/helpers_test.py
new file mode 100644
index 00000000..14b8540d
--- /dev/null
+++ b/headphones/helpers_test.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+from unittestcompat import TestCase
+from headphones.helpers import clean_name
+
+
+class HelpersTest(TestCase):
+
+ def test_clean_name(self):
+ """helpers: check correctness of clean_name() function"""
+ cases = {
+ u' Weiße & rose ': 'Weisse and rose',
+ u'Multiple / spaces': 'Multiple spaces',
+ u'Kevin\'s m²': 'Kevins m2',
+ u'Symphonęy Nº9': 'Symphoney No.9',
+ u'ÆæßðÞIJij': u'AeaessdThIJıj',
+ u'Obsessió (Cerebral Apoplexy remix)': 'obsessio cerebral '
+ 'apoplexy remix',
+ u'Doktór Hałabała i siedmiu zbojów': 'doktor halabala i siedmiu '
+ 'zbojow',
+ u'Arbetets Söner och Döttrar': 'arbetets soner och dottrar',
+ u'Björk Guðmundsdóttir': 'bjork gudmundsdottir',
+ u'L\'Arc~en~Ciel': 'larc en ciel',
+ u'Orquesta de la Luz (オルケスタ・デ・ラ・ルス)':
+ u'Orquesta de la Luz オルケスタ デ ラ ルス'
+
+ }
+ for first, second in cases.iteritems():
+ nf = clean_name(first).lower()
+ ns = clean_name(second).lower()
+ self.assertEqual(
+ nf, ns, u"check cleaning of case (%s,"
+ u"%s)" % (nf, ns)
+ )
+
+ def test_clean_name_nonunicode(self):
+ """helpers: check if clean_name() works on non-unicode input"""
+ input = 'foo $ bar/BAZ'
+ test = clean_name(input).lower()
+ expected = 'foo bar baz'
+ self.assertEqual(
+ test, expected, "check clean_name() works on non-unicode"
+ )
+ input = 'fóó $ BAZ'
+ test = clean_name(input).lower()
+ expected = clean_name('%fóó baz ').lower()
+ self.assertEqual(
+ test, expected, "check clean_name() with narrow non-ascii input"
+ )
diff --git a/headphones/importer.py b/headphones/importer.py
index 3cc7f141..3b92ca11 100644
--- a/headphones/importer.py
+++ b/headphones/importer.py
@@ -80,9 +80,8 @@ def artistlist_to_mbids(artistlist, forced=False):
bl_artist = myDB.action('SELECT * FROM blacklist WHERE ArtistID=?',
[artistid]).fetchone()
if bl_artist or artistid in blacklisted_special_artists:
- logger.info(
- "Artist ID for '%s' is either blacklisted or Various Artists. To add artist, you must do it manually (Artist ID: %s)" % (
- artist, artistid))
+ logger.info("Artist ID for '%s' is either blacklisted or Various Artists. To add artist, you must "
+ "do it manually (Artist ID: %s)" % (artist, artistid))
continue
# Add to database if it doesn't exist
@@ -225,9 +224,8 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
myDB.action("DELETE FROM tracks WHERE AlbumID=?", [items['AlbumID']])
myDB.action("DELETE FROM alltracks WHERE AlbumID=?", [items['AlbumID']])
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [items['AlbumID']])
- logger.info(
- "[%s] Removing all references to release group %s to reflect MusicBrainz refresh" % (
- artist['artist_name'], items['AlbumID']))
+ logger.info("[%s] Removing all references to release group %s to reflect MusicBrainz refresh" % (
+ artist['artist_name'], items['AlbumID']))
if not extrasonly:
force_repackage = 1
else:
@@ -259,14 +257,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
new_release_group = True
if new_release_group:
- logger.info("[%s] Now adding: %s (New Release Group)" % (
- artist['artist_name'], rg['title']))
+ logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid, includeExtras)
else:
if check_release_date is None or check_release_date == u"None":
- logger.info("[%s] Now updating: %s (No Release Date)" % (
- artist['artist_name'], rg['title']))
+ logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid, includeExtras, True)
else:
if len(check_release_date) == 10:
@@ -314,8 +310,8 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
# Build the dictionary for the fullreleaselist
for items in find_hybrid_releases:
- if items['ReleaseID'] != rg[
- 'id']: # don't include hybrid information, since that's what we're replacing
+ # don't include hybrid information, since that's what we're replacing
+ if items['ReleaseID'] != rg['id']:
hybrid_release_id = items['ReleaseID']
newValueDict = {"ArtistID": items['ArtistID'],
"ArtistName": items['ArtistName'],
@@ -349,11 +345,11 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
try:
hybridrelease = getHybridRelease(fullreleaselist)
logger.info('[%s] Packaging %s releases into hybrid title' % (
- artist['artist_name'], rg['title']))
+ artist['artist_name'], rg['title']))
except Exception as e:
errors = True
logger.warn('[%s] Unable to get hybrid release information for %s: %s' % (
- artist['artist_name'], rg['title'], e))
+ artist['artist_name'], rg['title'], e))
continue
# Use the ReleaseGroupID as the ReleaseID for the hybrid release to differentiate it
@@ -374,8 +370,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
for track in hybridrelease['Tracks']:
- cleanname = helpers.cleanName(
- artist['artist_name'] + ' ' + rg['title'] + ' ' + track['title'])
+ cleanname = helpers.clean_name(artist['artist_name'] + ' ' + rg['title'] + ' ' + track['title'])
controlValueDict = {"TrackID": track['id'],
"ReleaseID": rg['id']}
@@ -504,13 +499,13 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
if rg_exists:
if rg_exists['Status'] == 'Skipped' and (
(have_track_count / float(total_track_count)) >= (
- headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
+ headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?',
['Downloaded', rg['id']])
marked_as_downloaded = True
else:
if (have_track_count / float(total_track_count)) >= (
- headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0):
+ headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0):
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?',
['Downloaded', rg['id']])
marked_as_downloaded = True
@@ -527,7 +522,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
else:
if skip_log == 0:
logger.info(u"[%s] No new releases, so no changes made to %s" % (
- artist['artist_name'], rg['title']))
+ artist['artist_name'], rg['title']))
time.sleep(3)
finalize_update(artistid, artist['artist_name'], errors)
@@ -541,7 +536,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
if errors:
logger.info(
"[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (
- artist['artist_name'], artist['artist_name']))
+ artist['artist_name'], artist['artist_name']))
else:
myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']])
logger.info(u"Updating complete for: %s" % artist['artist_name'])
@@ -710,7 +705,7 @@ def addReleaseById(rid, rgid=None):
myDB.action('INSERT INTO releases VALUES( ?, ?)', [rid, release_dict['rgid']])
for track in release_dict['tracks']:
- cleanname = helpers.cleanName(
+ cleanname = helpers.clean_name(
release_dict['artist_name'] + ' ' + release_dict['rg_title'] + ' ' + track['title'])
controlValueDict = {"TrackID": track['id'],
diff --git a/headphones/librarysync.py b/headphones/librarysync.py
index 367d160c..ba246ff0 100644
--- a/headphones/librarysync.py
+++ b/headphones/librarysync.py
@@ -138,7 +138,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
# TODO: skip adding songs without the minimum requisite information (just a matter of putting together the right if statements)
if f_artist and f.album and f.title:
- CleanName = helpers.cleanName(f_artist + ' ' + f.album + ' ' + f.title)
+ CleanName = helpers.clean_name(f_artist + ' ' + f.album + ' ' + f.title)
else:
CleanName = None
@@ -170,7 +170,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
new_song_count += 1
else:
if check_exist_song['ArtistName'] != f_artist or check_exist_song[
- 'AlbumTitle'] != f.album or check_exist_song['TrackTitle'] != f.title:
+ 'AlbumTitle'] != f.album or check_exist_song['TrackTitle'] != f.title:
# Important track metadata has been modified, need to run matcher again
if f_artist and f_artist != check_exist_song['ArtistName']:
new_artists.append(f_artist)
@@ -209,7 +209,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
# Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid)
# When we insert into the database, the tracks with the most specific information will overwrite the more general matches
- ##############song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID'])
+ # song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID'])
song_list = helpers.multikeysort(song_list, ['ArtistName', 'AlbumTitle'])
# We'll use this to give a % completion, just because the track matching might take a while
@@ -317,7 +317,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
newValueDict2 = {'Matched': "Failed"}
myDB.upsert("have", newValueDict2, controlValueDict2)
- #######myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
+ # myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING,
'replace'))
@@ -332,15 +332,15 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
# There was a bug where artists with special characters (-,') would show up in new artists.
artist_list = [
x for x in unique_artists
- if helpers.cleanName(x).lower() not in [
- helpers.cleanName(y[0]).lower()
+ if helpers.clean_name(x).lower() not in [
+ helpers.clean_name(y[0]).lower()
for y in current_artists
]
]
artists_checked = [
x for x in unique_artists
- if helpers.cleanName(x).lower() in [
- helpers.cleanName(y[0]).lower()
+ if helpers.clean_name(x).lower() in [
+ helpers.clean_name(y[0]).lower()
for y in current_artists
]
]
@@ -353,8 +353,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
len(myDB.select(
'SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL',
[artist])) + len(myDB.select(
- 'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
- [artist]))
+ 'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
+ [artist]))
)
# Note: some people complain about having "artist have tracks" > # of tracks total in artist official releases
# (can fix by getting rid of second len statement)
@@ -382,8 +382,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
havetracks = len(
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
[ArtistID])) + len(myDB.select(
- 'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
- [ArtistName]))
+ 'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
+ [ArtistName]))
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
if not append:
diff --git a/headphones/logger.py b/headphones/logger.py
index b01e880f..1eaac7cc 100644
--- a/headphones/logger.py
+++ b/headphones/logger.py
@@ -72,7 +72,7 @@ def listener():
# http://stackoverflow.com/questions/2009278 for more information.
if e.errno == errno.EACCES:
logger.warning("Multiprocess logging disabled, because "
- "current user cannot map shared memory. You won't see any" \
+ "current user cannot map shared memory. You won't see any"
"logging generated by the worker processed.")
# Multiprocess logging may be disabled.
diff --git a/headphones/mb.py b/headphones/mb.py
index 38b952e6..a0f80a30 100644
--- a/headphones/mb.py
+++ b/headphones/mb.py
@@ -287,7 +287,7 @@ def getArtist(artistid, extrasonly=False):
except musicbrainzngs.WebServiceError as e:
logger.warn(
'Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (
- artistid, str(e)))
+ artistid, str(e)))
mb_lock.snooze(5)
except Exception as e:
pass
@@ -354,7 +354,7 @@ def getArtist(artistid, extrasonly=False):
except musicbrainzngs.WebServiceError as e:
logger.warn(
'Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (
- artistid, str(e)))
+ artistid, str(e)))
mb_lock.snooze(5)
for rg in mb_extras_list:
@@ -384,9 +384,9 @@ def getSeries(seriesid):
except musicbrainzngs.WebServiceError as e:
logger.warn(
'Attempt to retrieve series information from MusicBrainz failed for seriesid: %s (%s)' % (
- seriesid, str(e)))
+ seriesid, str(e)))
mb_lock.snooze(5)
- except Exception as e:
+ except Exception:
pass
if not series:
@@ -425,7 +425,7 @@ def getReleaseGroup(rgid):
except musicbrainzngs.WebServiceError as e:
logger.warn(
'Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (
- rgid, str(e)))
+ rgid, str(e)))
mb_lock.snooze(5)
if not releaseGroup:
@@ -453,7 +453,7 @@ def getRelease(releaseid, include_artist_info=True):
except musicbrainzngs.WebServiceError as e:
logger.warn(
'Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (
- releaseid, str(e)))
+ releaseid, str(e)))
mb_lock.snooze(5)
if not results:
@@ -482,7 +482,7 @@ def getRelease(releaseid, include_artist_info=True):
release['rg_type'] = unicode(results['release-group']['type'])
if release['rg_type'] == 'Album' and 'secondary-type-list' in results[
- 'release-group']:
+ 'release-group']:
secondary_type = unicode(results['release-group']['secondary-type-list'][0])
if secondary_type != release['rg_type']:
release['rg_type'] = secondary_type
@@ -528,7 +528,7 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
except musicbrainzngs.WebServiceError as e:
logger.warn(
'Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (
- rgid, str(e)))
+ rgid, str(e)))
mb_lock.snooze(5)
return False
@@ -637,7 +637,7 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
for track in release['Tracks']:
- cleanname = helpers.cleanName(
+ cleanname = helpers.clean_name(
release['ArtistName'] + ' ' + release['AlbumTitle'] + ' ' + track['title'])
controlValueDict = {"TrackID": track['id'],
@@ -675,14 +675,14 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
num_new_releases = num_new_releases + 1
if album_checker:
logger.info('[%s] Existing release %s (%s) updated' % (
- release['ArtistName'], release['AlbumTitle'], rel_id_check))
+ release['ArtistName'], release['AlbumTitle'], rel_id_check))
else:
logger.info('[%s] New release %s (%s) added' % (
- release['ArtistName'], release['AlbumTitle'], rel_id_check))
+ release['ArtistName'], release['AlbumTitle'], rel_id_check))
if force_repackage1 == 1:
num_new_releases = -1
logger.info('[%s] Forcing repackage of %s, since dB releases have been removed' % (
- release['ArtistName'], release_title))
+ release['ArtistName'], release_title))
else:
num_new_releases = num_new_releases
diff --git a/headphones/metadata.py b/headphones/metadata.py
new file mode 100644
index 00000000..f5ddf4bc
--- /dev/null
+++ b/headphones/metadata.py
@@ -0,0 +1,376 @@
+# encoding=utf8
+# 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 .
+"""
+Track/album metadata handling routines.
+"""
+
+from __future__ import print_function
+from beets.mediafile import MediaFile, UnreadableFileError
+import headphones
+from headphones import logger
+import os.path
+import datetime
+
+__author__ = "Andrzej Ciarkowski "
+
+
+class MetadataDict(dict):
+ """
+ Dictionary which allows for case-insensitive, but case-preserving lookup,
+ allowing to put different values under $Album and $album, but still
+ finding some value if only single key is present and called with any
+ variation of the name's case.
+
+ Keeps case-sensitive mapping in superclass dict, and case-insensitive (
+ lowercase) in member variable self._lower. If case-sensitive lookup
+ fails, another case-insensitive attempt is made.
+ """
+ def __setitem__(self, key, value):
+ super(MetadataDict, self).__setitem__(key, value)
+ self._lower.__setitem__(key.lower(), value)
+
+ def add_items(self, items):
+ # type: (Iterable[Tuple[Any,Any]])->None
+ """
+ Add (key,value) pairs to this dictionary using iterable as an input.
+ :param items: input items.
+ """
+ for key, value in items:
+ self.__setitem__(key, value)
+
+ def __init__(self, seq=None, **kwargs):
+ if isinstance(seq, MetadataDict):
+ super(MetadataDict, self).__init__(seq)
+ self._lower = dict(seq._lower)
+ else:
+ super(MetadataDict, self).__init__()
+ self._lower = {}
+ if seq is not None:
+ try:
+ self.add_items(seq.iteritems())
+ except KeyError:
+ self.add_items(seq)
+
+ def __getitem__(self, item):
+ try:
+ return super(MetadataDict, self).__getitem__(item)
+ except KeyError:
+ return self._lower.__getitem__(item.lower())
+
+ def __contains__(self, item):
+ return self._lower.__contains__(item.lower())
+
+
+class Vars:
+ """
+ Metadata $variable names (only ones set explicitly by headphones).
+ """
+ DISC = '$Disc'
+ TRACK = '$Track'
+ TITLE = '$Title'
+ ARTIST = '$Artist'
+ SORT_ARTIST = '$SortArtist'
+ ALBUM = '$Album'
+ YEAR = '$Year'
+ DATE = '$Date'
+ EXTENSION = '$Extension'
+ ORIGINAL_FOLDER = '$OriginalFolder'
+ FIRST_LETTER = '$First'
+ TYPE = '$Type'
+ TITLE_LOWER = TITLE.lower()
+ ARTIST_LOWER = ARTIST.lower()
+ SORT_ARTIST_LOWER = SORT_ARTIST.lower()
+ ALBUM_LOWER = ALBUM.lower()
+ ORIGINAL_FOLDER_LOWER = ORIGINAL_FOLDER.lower()
+ FIRST_LETTER_LOWER = FIRST_LETTER.lower()
+ TYPE_LOWER = TYPE.lower()
+
+
+def _verify_var_type(val):
+ """
+ Check if type of value is allowed as a variable in pathname substitution.
+ """
+ return isinstance(val, (basestring, int, float, datetime.date))
+
+
+def _as_str(val):
+ if isinstance(val, basestring):
+ return val
+ else:
+ return str(val)
+
+
+def _media_file_to_dict(mf, d):
+ # type: (MediaFile, MutableMapping[basestring,basestring])->None
+ """
+ Populate dict with tags read from media file.
+ """
+ for fld in mf.readable_fields():
+ if 'art' == fld:
+ # skip embedded artwork as it's a BLOB
+ continue
+ val = getattr(mf, fld)
+ if val is None:
+ val = ''
+ # include only types with meaningful string representation
+ if _verify_var_type(val):
+ d['$' + fld] = _as_str(val)
+
+
+def _row_to_dict(row, d):
+ """
+ Populate dict with database row fields.
+ """
+ for fld in row.keys():
+ val = row[fld]
+ if val is None:
+ val = ''
+ if _verify_var_type(val):
+ d['$' + fld] = _as_str(val)
+
+
+def _date_year(release):
+ # type: (sqlite3.Row)->Tuple[str,str]
+ """
+ Extract release date and year from database row
+ """
+ try:
+ date = release['ReleaseDate']
+ except TypeError:
+ date = ''
+
+ if date is not None:
+ year = date[:4]
+ else:
+ year = ''
+ return date, year
+
+
+def _lower(s):
+ # type: basestring->basestring
+ """
+ Return s.lower() if not None
+ :param s:
+ :return:
+ """
+ if s:
+ return s.lower()
+ return None
+
+
+def file_metadata(path, release):
+ # type: (str,sqlite3.Row)->Tuple[Mapping[str,str],bool]
+ """
+ Prepare metadata dictionary for path substitution, based on file name,
+ the tags stored within it and release info from the db.
+ :param path: media file path
+ :param release: database row with release info
+ :return: pair (dict,boolean indicating if Vars.TITLE is taken from tags or
+ file name). (None,None) if unable to parse the media file.
+ """
+ try:
+ f = MediaFile(path)
+ except UnreadableFileError as ex:
+ logger.info("MediaFile couldn't parse: %s (%s)",
+ path.decode(headphones.SYS_ENCODING, 'replace'),
+ str(ex))
+ return None, None
+
+ res = MetadataDict()
+ # add existing tags first, these will get overwritten by musicbrainz from db
+ _media_file_to_dict(f, res)
+ # raw database fields come next
+ _row_to_dict(release, res)
+
+ date, year = _date_year(release)
+ if not f.disc:
+ disc_number = ''
+ else:
+ disc_number = '%d' % f.disc
+
+ if not f.track:
+ track_number = ''
+ else:
+ track_number = '%02d' % f.track
+
+ if not f.title:
+ basename = os.path.basename(
+ path.decode(headphones.SYS_ENCODING, 'replace'))
+ title = os.path.splitext(basename)[0]
+ from_metadata = False
+ else:
+ title = f.title
+ from_metadata = True
+
+ ext = os.path.splitext(path)[1]
+ if release['ArtistName'] == "Various Artists" and f.artist:
+ artist_name = f.artist
+ else:
+ artist_name = release['ArtistName']
+
+ if artist_name and artist_name.startswith('The '):
+ sort_name = artist_name[4:] + ", The"
+ else:
+ sort_name = artist_name
+
+ album_title = release['AlbumTitle']
+ override_values = {
+ Vars.DISC: disc_number,
+ Vars.TRACK: track_number,
+ Vars.TITLE: title,
+ Vars.ARTIST: artist_name,
+ Vars.SORT_ARTIST: sort_name,
+ Vars.ALBUM: album_title,
+ Vars.YEAR: year,
+ Vars.DATE: date,
+ Vars.EXTENSION: ext,
+ Vars.TITLE_LOWER: _lower(title),
+ Vars.ARTIST_LOWER: _lower(artist_name),
+ Vars.SORT_ARTIST_LOWER: _lower(sort_name),
+ Vars.ALBUM_LOWER: _lower(album_title),
+ }
+ res.add_items(override_values.iteritems())
+ return res, from_metadata
+
+
+def _intersect(d1, d2):
+ # type: (Mapping,Mapping)->Mapping
+ """
+ Create intersection (common part) of two dictionaries.
+ """
+ res = {}
+ for key, val in d1.iteritems():
+ if key in d2 and d2[key] == val:
+ res[key] = val
+ return res
+
+
+def album_metadata(path, release, common_tags):
+ # type: (str,sqlite3.Row,Mapping[str,str])->Mapping[str,str]
+ """
+ Prepare metadata dictionary for path substitution of album folder.
+ :param path: album path to prepare metadata for.
+ :param release: database row with release properties.
+ :param common_tags: common set of tags gathered from media files.
+ :return: metadata dictionary with substitution variables for rendering path.
+ """
+ date, year = _date_year(release)
+ artist = release['ArtistName']
+ if artist:
+ artist = artist.replace('/', '_')
+ album = release['AlbumTitle']
+ if album:
+ album = album.replace('/', '_')
+ release_type = release['Type']
+ if release_type:
+ release_type = release_type.replace('/', '_')
+
+ if artist and artist.startswith('The '):
+ sort_name = artist[4:] + ", The"
+ else:
+ sort_name = artist
+
+ if not sort_name or sort_name[0].isdigit():
+ first_char = u'0-9'
+ else:
+ first_char = sort_name[0]
+
+ orig_folder = u''
+ for r, d, f in os.walk(path):
+ try:
+ orig_folder = os.path.basename(
+ os.path.normpath(r).decode(headphones.SYS_ENCODING, 'replace'))
+ break
+ except:
+ pass
+
+ override_values = {
+ Vars.ARTIST: artist,
+ Vars.SORT_ARTIST: sort_name,
+ Vars.ALBUM: album,
+ Vars.YEAR: year,
+ Vars.DATE: date,
+ Vars.TYPE: release_type,
+ Vars.ORIGINAL_FOLDER: orig_folder,
+ Vars.FIRST_LETTER: first_char.upper(),
+ Vars.ARTIST_LOWER: _lower(artist),
+ Vars.SORT_ARTIST_LOWER: _lower(sort_name),
+ Vars.ALBUM_LOWER: _lower(album),
+ Vars.TYPE_LOWER: _lower(release_type),
+ Vars.FIRST_LETTER_LOWER: _lower(first_char),
+ Vars.ORIGINAL_FOLDER_LOWER: _lower(orig_folder)
+ }
+ res = MetadataDict(common_tags)
+ res.add_items(override_values.iteritems())
+ return res
+
+
+def albumart_metadata(release, common_tags):
+ # type: (sqlite3.Row,Mapping)->Mapping
+ """
+ Prepare metadata dictionary for path subtitution of album art file.
+ :param release: database row with release properties.
+ :param common_tags: common set of tags gathered from media files.
+ :return: metadata dictionary with substitution variables for rendering path.
+ """
+ date, year = _date_year(release)
+ artist = release['ArtistName']
+ album = release['AlbumTitle']
+
+ override_values = {
+ Vars.ARTIST: artist,
+ Vars.ALBUM: album,
+ Vars.YEAR: year,
+ Vars.DATE: date,
+ Vars.ARTIST_LOWER: _lower(artist),
+ Vars.ALBUM_LOWER: _lower(album)
+ }
+ res = MetadataDict(common_tags)
+ res.add_items(override_values.iteritems())
+ return res
+
+
+class AlbumMetadataBuilder(object):
+ """
+ Facilitates building of album metadata as a common set of tags retrieved
+ from media files.
+ """
+
+ def __init__(self):
+ self._common = None
+
+ def add_media_file(self, mf):
+ # type: (Mapping)->None
+ """
+ Add metadata tags read from media file to album metadata.
+ :param mf: MediaFile
+ """
+ md = {}
+ _media_file_to_dict(mf, md)
+ if self._common is None:
+ self._common = md
+ else:
+ self._common = _intersect(self._common, md)
+
+ def build(self):
+ # type: (None)->Mapping
+ """
+ Build case-insensitive, case-preserving dict from gathered metadata
+ tags.
+ :return: dictinary-like object filled with $variables based on common
+ tags.
+ """
+ return MetadataDict(self._common)
diff --git a/headphones/metadata_test.py b/headphones/metadata_test.py
new file mode 100644
index 00000000..37d07c12
--- /dev/null
+++ b/headphones/metadata_test.py
@@ -0,0 +1,175 @@
+# encoding=utf8
+# 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 .
+"""
+Test module for metadata.
+"""
+import headphones as _h
+import headphones.metadata as _md
+import headphones.helpers as _hp
+from headphones.metadata import MetadataDict
+import datetime
+
+from unittestcompat import TestCase
+
+
+__author__ = "Andrzej Ciarkowski "
+
+
+class _MockMediaFile(object):
+
+ def __init__(self, artist, album, year, track, title, label):
+ self.artist = artist
+ self.album = album
+ self.year = year
+ self.track = track
+ self.title = title
+ self.label = label
+ self.art = 'THIS IS ART BLOB'
+
+ @classmethod
+ def readable_fields(cls):
+ return 'artist', 'album', 'year', 'track', 'title', 'label', 'art'
+
+
+class _MockDatabaseRow(object):
+
+ def __init__(self, d):
+ self._dict = dict(d)
+
+ def keys(self):
+ return self._dict.iterkeys()
+
+ def __getitem__(self, item):
+ return self._dict[item]
+
+
+class MetadataTest(TestCase):
+ """
+ Tests for metadata module.
+ """
+
+ def test_metadata_dict_ci(self):
+ """MetadataDict: case-insensitive lookup"""
+ expected = u'naïve'
+ key_var = '$TitlE'
+ m = MetadataDict({key_var.lower(): u'naïve'})
+ self.assertFalse('$track' in m)
+ self.assertTrue('$tITLe' in m, "cross-case lookup with 'in'")
+ self.assertEqual(m[key_var], expected, "cross-case lookup success")
+ self.assertEqual(m[key_var.lower()], expected, "same-case lookup "
+ "succes")
+
+ def test_metadata_dict_cs(self):
+ """MetadataDice: case-preserving lookup"""
+ expected_var = u'NaïVe'
+ key_var = '$TitlE'
+ m = MetadataDict({
+ key_var.lower(): expected_var.lower(),
+ key_var: expected_var
+ })
+ self.assertFalse('$track' in m)
+ self.assertTrue('$tITLe' in m, "cross-case lookup with 'in'")
+ self.assertEqual(m[key_var.lower()], expected_var.lower(),
+ "case-preserving lookup lower")
+ self.assertEqual(m[key_var], expected_var,
+ "case-preserving lookup variable")
+
+ def test_dict_intersect(self):
+ """metadata: check dictionary intersect function validity"""
+ d1 = {
+ 'one': 'one',
+ 'two': 'two',
+ 'three': 'zonk'
+ }
+ d2 = {
+ 'two': 'two',
+ 'three': 'three'
+ }
+ expected = {
+ 'two': 'two'
+ }
+ self.assertItemsEqual(
+ expected, _md._intersect(d1, d2), "check dictionary intersection "
+ "is common part indeed"
+ )
+ del d1['two']
+ expected = {}
+ self.assertItemsEqual(
+ expected, _md._intersect(d1, d2), "check intersection empty"
+ )
+
+ def test_album_metadata_builder(self):
+ """AlbumMetadataBuilder: check validity"""
+ mb = _md.AlbumMetadataBuilder()
+ f1 = _MockMediaFile('artist', 'album', 2000, 1, 'track1', 'Ant-Zen')
+ mb.add_media_file(f1)
+ f2 = _MockMediaFile('artist', 'album', 2000, 2, 'track2', 'Ant-Zen')
+ mb.add_media_file(f2)
+
+ md = mb.build()
+ expected = {
+ _md.Vars.ARTIST_LOWER: 'artist',
+ _md.Vars.ALBUM_LOWER: 'album',
+ _md.Vars.YEAR.lower(): 2000,
+ '$label': 'Ant-Zen'
+ }
+ self.assertItemsEqual(
+ expected, md, "check AlbumMetadataBuilder validity"
+ )
+
+ def test_populate_from_row(self):
+ """metadata: check populating metadata from database row"""
+ row = _MockDatabaseRow({
+ 'ArtistName': 'artist',
+ 'AlbumTitle': 'album',
+ 'ReleaseDate': datetime.date(2004, 11, 28),
+ 'Variation': 5,
+ 'WrongTyped': complex(1, -1)
+ })
+ md = _md.MetadataDict()
+ _md._row_to_dict(row, md)
+ expected = {
+ '$ArtistName': 'artist',
+ '$AlbumTitle': 'album',
+ '$ReleaseDate': '2004-11-28',
+ '$Variation': '5'
+ }
+ self.assertItemsEqual(expected, md, "check _row_to_dict() valid")
+
+ def test_album_metadata_with_None(self):
+ """metadata: check handling of None metadata values"""
+ row = _MockDatabaseRow({
+ 'ArtistName': 'artist',
+ 'AlbumTitle': 'Album',
+ 'Type': None,
+ 'ReleaseDate': None,
+ })
+ mb = _md.AlbumMetadataBuilder()
+ f1 = _MockMediaFile('artist', None, None, None, None, None)
+ mb.add_media_file(f1)
+ f2 = _MockMediaFile('artist', None, None, 2, 'track2', None)
+ mb.add_media_file(f2)
+ md = _md.album_metadata("/music/Artist - Album [2002]", row, mb.build())
+
+ # tests don't undergo normal Headphones init, SYS_ENCODING is not set
+ if not _h.SYS_ENCODING:
+ _h.SYS_ENCODING = 'UTF-8'
+
+ res = _hp.replace_all(
+ "/music/$First/$Artist/$Artist - $Album{ [$Year]}", md, True)
+
+ self.assertEqual(res, u"/music/A/artist/artist - Album",
+ "check correct rendering of None via replace_all()")
diff --git a/headphones/music_encoder.py b/headphones/music_encoder.py
index 12a71797..b51001f6 100644
--- a/headphones/music_encoder.py
+++ b/headphones/music_encoder.py
@@ -129,7 +129,7 @@ def encode(albumPath):
os.path.splitext(music)[1], music)
else:
if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.mp3') and (
- int(infoMusic.bitrate / 1000) <= headphones.CONFIG.BITRATE):
+ int(infoMusic.bitrate / 1000) <= headphones.CONFIG.BITRATE):
logger.info('%s has bitrate <= %skb, will not be re-encoded', music,
headphones.CONFIG.BITRATE)
else:
diff --git a/headphones/notifiers.py b/headphones/notifiers.py
index d22303cc..1b885335 100644
--- a/headphones/notifiers.py
+++ b/headphones/notifiers.py
@@ -54,6 +54,7 @@ class GROWL(object):
# Split host and port
if self.host == "":
host, port = "localhost", 23053
+
if ":" in self.host:
host, port = self.host.split(':', 1)
port = int(port)
@@ -245,9 +246,8 @@ class XBMC(object):
for host in hosts:
logger.info('Sending notification command to XMBC @ ' + host)
try:
- version = \
- self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})[
- 'version']['major']
+ version = self._sendjson(host, 'Application.GetProperties',
+ {'properties': ['version']})['version']['major']
if version < 12: # Eden
notification = header + "," + message + "," + time + "," + albumartpath
@@ -383,9 +383,8 @@ class Plex(object):
for host in hosts:
logger.info('Sending notification command to Plex client @ ' + host)
try:
- version = \
- self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})[
- 'version']['major']
+ version = self._sendjson(host, 'Application.GetProperties',
+ {'properties': ['version']})['version']['major']
if version < 12: # Eden
notification = header + "," + message + "," + time + "," + albumartpath
diff --git a/headphones/nzbget.py b/headphones/nzbget.py
index 0a5a27c0..779558ef 100644
--- a/headphones/nzbget.py
+++ b/headphones/nzbget.py
@@ -49,11 +49,11 @@ def sendNZB(nzb):
nzbGetRPC = xmlrpclib.ServerProxy(url)
try:
if nzbGetRPC.writelog("INFO", "headphones connected to drop of %s any moment now." % (
- nzb.name + ".nzb")):
+ nzb.name + ".nzb")):
logger.debug(u"Successfully connected to NZBget")
else:
logger.info(u"Successfully connected to NZBget, but unable to send a message" % (
- nzb.name + ".nzb"))
+ nzb.name + ".nzb"))
except httplib.socket.error:
logger.error(
diff --git a/headphones/pathrender.py b/headphones/pathrender.py
index 162c6956..48798dd7 100644
--- a/headphones/pathrender.py
+++ b/headphones/pathrender.py
@@ -13,22 +13,23 @@
#
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see .
-'''Path pattern substitution module, see details below for syntax.
+"""
+Path pattern substitution module, see details below for syntax.
- The pattern matching is loosely based on foobar2000 pattern syntax,
- i.e. the notion of escaping characters with \' and optional elements
- enclosed in square brackets [] is taken from there while the
- substitution variable names are Perl-ish or sh-ish. The following
- syntax elements are supported:
- * escaped literal strings, that is everything that is enclosed
- within single quotes (like \'this\');
- * substitution variables, which start with dollar sign ($) and
- extend until next non-alphanumeric+underscore character
- (like $This and $5_that).
- * optional elements enclosed in curly braces, which render
- nonempty value only if any variable or optional inside returned
- nonempty value, ignoring literals (like {\'[\'$That\']\'}).
-'''
+The pattern matching is loosely based on foobar2000 pattern syntax,
+i.e. the notion of escaping characters with \' and optional elements
+enclosed in square brackets [] is taken from there while the
+substitution variable names are Perl-ish or sh-ish. The following
+syntax elements are supported:
+* escaped literal strings, that is everything that is enclosed
+ within single quotes (like 'this');
+* substitution variables, which start with dollar sign ($) and
+ extend until next non-alphanumeric+underscore character
+ (like $This and $5_that).
+* optional elements enclosed in curly braces, which render
+ nonempty value only if any variable or optional inside returned
+ nonempty value, ignoring literals (like {'{'$That'}'}).
+"""
from __future__ import print_function
from enum import Enum
@@ -42,6 +43,9 @@ class _PatternElement(object):
'''Format this _PatternElement into string using provided substitution dictionary.'''
raise NotImplementedError()
+ def __ne__(self, other):
+ return not self == other
+
class _Generator(_PatternElement):
# pylint: disable=abstract-method
@@ -57,11 +61,23 @@ class _Replacement(_Generator):
def render(self, replacement):
# type: (Mapping[str,str]) -> str
- return replacement.get(self._pattern, self._pattern)
+ res = replacement.get(self._pattern, self._pattern)
+ if res is None:
+ return ''
+ else:
+ return res
def __str__(self):
return self._pattern
+ @property
+ def pattern(self):
+ return self._pattern
+
+ def __eq__(self, other):
+ return isinstance(other, _Replacement) and \
+ self._pattern == other.pattern
+
class _LiteralText(_PatternElement):
'''Just a plain piece of text to be rendered "as is".'''
@@ -76,6 +92,13 @@ class _LiteralText(_PatternElement):
def __str__(self):
return self._text
+ @property
+ def text(self):
+ return self._text
+
+ def __eq__(self, other):
+ return isinstance(other, _LiteralText) and self._text == other.text
+
class _OptionalBlock(_Generator):
'''Optional block will render its contents only if any _Generator in its scope did return non-empty result.'''
@@ -87,11 +110,17 @@ class _OptionalBlock(_Generator):
def render(self, replacement):
# type: (Mapping[str,str]) -> str
res = [(isinstance(x, _Generator), x.render(replacement)) for x in self._scope]
- if any((t[0] and len(t[1]) != 0) for t in res):
+ if any((t[0] and t[1] is not None and len(t[1]) != 0) for t in res):
return u"".join(t[1] for t in res)
else:
return u""
+ def __eq__(self, other):
+ """
+ :type other: _OptionalBlock
+ """
+ return isinstance(other, _OptionalBlock) and self._scope == other._scope
+
_OPTIONAL_START = u'{'
_OPTIONAL_END = u'}'
@@ -230,8 +259,9 @@ def render(pattern, replacement):
p = Pattern(pattern)
return p(replacement), p.warnings
+
if __name__ == "__main__":
# primitive test ;)
- p = Pattern(u"[$Disc.]$Track - $Artist - $Title[ '['$Year']'")
+ p = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]}")
d = {'$Disc': '', '$Track': '05', '$Artist': u'Grzegżółka', '$Title': u'Błona kapłona', '$Year': '2019'}
- print(p(d).encode('utf8'), p.warnings)
+ assert p(d) == u"05 - Grzegżółka - Błona kapłona [2019]"
diff --git a/headphones/pathrender_test.py b/headphones/pathrender_test.py
new file mode 100644
index 00000000..9bcb7f4c
--- /dev/null
+++ b/headphones/pathrender_test.py
@@ -0,0 +1,96 @@
+# encoding=utf8
+# 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 .
+"""
+Test module for pathrender.
+"""
+import headphones.pathrender as _pr
+from headphones.pathrender import Pattern, Warnings
+
+from unittestcompat import TestCase
+
+
+__author__ = "Andrzej Ciarkowski "
+
+
+class PathRenderTest(TestCase):
+ """
+ Tests for pathrender module.
+ """
+
+ def test_parsing(self):
+ """pathrender: pattern parsing"""
+ pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]}")
+ expected = [
+ _pr._OptionalBlock([
+ _pr._Replacement(u"$Disc"),
+ _pr._LiteralText(u".")
+ ]),
+ _pr._Replacement(u"$Track"),
+ _pr._LiteralText(u" - "),
+ _pr._Replacement(u"$Artist"),
+ _pr._LiteralText(u" - "),
+ _pr._Replacement(u"$Title"),
+ _pr._OptionalBlock([
+ _pr._LiteralText(u" ["),
+ _pr._Replacement(u"$Year"),
+ _pr._LiteralText(u"]")
+ ])
+ ]
+ self.assertEqual(expected, pattern._pattern)
+ self.assertItemsEqual([], pattern.warnings)
+
+ def test_parsing_warnings(self):
+ """pathrender: pattern parsing with warnings"""
+ pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]")
+ self.assertEqual(set([Warnings.UNCLOSED_OPTIONAL]), pattern.warnings)
+ pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]'}")
+ self.assertEqual(set([Warnings.UNCLOSED_ESCAPE, Warnings.UNCLOSED_OPTIONAL]), pattern.warnings)
+
+ def test_replacement(self):
+ """pathrender: _Replacement variable substitution"""
+ r = _pr._Replacement(u"$Title")
+ subst = {'$Title': 'foo', '$Track': 'bar'}
+ res = r.render(subst)
+ self.assertEqual(res, u'foo', 'check valid replacement')
+ subst = {}
+ res = r.render(subst)
+ self.assertEqual(res, u'$Title', 'check missing replacement')
+ subst = {'$Title': None}
+ res = r.render(subst)
+ self.assertEqual(res, '', 'check render() works with None')
+
+ def test_literal(self):
+ """pathrender: _Literal text rendering"""
+ l = _pr._LiteralText(u"foo")
+ subst = {'$foo': 'bar'}
+ res = l.render(subst)
+ self.assertEqual(res, 'foo')
+
+ def test_optional(self):
+ """pathrender: _OptionalBlock element processing"""
+ o = _pr._OptionalBlock([
+ _pr._Replacement(u"$Title"),
+ _pr._LiteralText(u".foobar")
+ ])
+ subst = {'$Title': 'foo', '$Track': 'bar'}
+ res = o.render(subst)
+ self.assertEqual(res, u'foo.foobar', 'check non-empty replacement')
+ subst = {'$Title': ''}
+ res = o.render(subst)
+ self.assertEqual(res, '', 'check empty replacement')
+ subst = {'$Title': None}
+ res = o.render(subst)
+ self.assertEqual(res, '', 'check render() works with None')
diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py
index 507a5571..ae38bdfb 100755
--- a/headphones/postprocessor.py
+++ b/headphones/postprocessor.py
@@ -30,6 +30,7 @@ from beetsplug import lyrics as beetslyrics
from headphones import notifiers, utorrent, transmission, deluge
from headphones import db, albumart, librarysync
from headphones import logger, helpers, request, mb, music_encoder
+from headphones import metadata
postprocessor_lock = threading.Lock()
@@ -46,7 +47,7 @@ def checkFolder():
if album['Kind'] == 'nzb':
download_dir = headphones.CONFIG.DOWNLOAD_DIR
else:
- if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
+ if headphones.CONFIG.DELUGE_DONE_DIRECTORY and headphones.CONFIG.TORRENT_DOWNLOADER == 3:
download_dir = headphones.CONFIG.DELUGE_DONE_DIRECTORY
else:
download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
@@ -108,8 +109,8 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
[release_dict['artist_id'], release_dict['artist_name']])
if not artist:
- logger.warn("Continuing would add new artist '%s' (ID %s), " \
- "but database is frozen. Will skip postprocessing for " \
+ logger.warn("Continuing would add new artist '%s' (ID %s), "
+ "but database is frozen. Will skip postprocessing for "
"album with rgid: %s", release_dict['artist_name'],
release_dict['artist_id'], albumid)
@@ -339,18 +340,16 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
downloaded_track_list.append(os.path.join(r, files))
+ builder = metadata.AlbumMetadataBuilder()
# Check if files are valid media files and are writable, before the steps
# below are executed. This simplifies errors and prevents unfinished steps.
for downloaded_track in downloaded_track_list:
try:
f = MediaFile(downloaded_track)
- if f is None:
- # this test is just to keep pyflakes from complaining about an unused variable
- return
+ builder.add_media_file(f)
except (FileTypeError, UnreadableFileError):
- logger.error("Track file is not a valid media file: %s. Not " \
- "continuing.", downloaded_track.decode(
- headphones.SYS_ENCODING, "replace"))
+ logger.error("Track file is not a valid media file: %s. Not continuing.",
+ downloaded_track.decode(headphones.SYS_ENCODING, "replace"))
return
except IOError:
logger.error("Unable to find media file: %s. Not continuing.")
@@ -371,13 +370,14 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
fp.seek(0)
except IOError as e:
logger.debug("Write check exact error: %s", e)
- logger.error("Track file is not writable. This is required " \
+ logger.error("Track file is not writable. This is required "
"for some post processing steps: %s. Not continuing.",
downloaded_track.decode(headphones.SYS_ENCODING, "replace"))
if new_folder:
shutil.rmtree(new_folder)
return
+ metadata_dict = builder.build()
# start encoding
if headphones.CONFIG.MUSIC_ENCODER:
downloaded_track_list = music_encoder.encode(albumpath)
@@ -413,7 +413,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
renameNFO(albumpath)
if headphones.CONFIG.ADD_ALBUM_ART and artwork:
- addAlbumArt(artwork, albumpath, release)
+ addAlbumArt(artwork, albumpath, release, metadata_dict)
if headphones.CONFIG.CORRECT_METADATA:
correctedMetadata = correctMetadata(albumid, release, downloaded_track_list)
@@ -433,7 +433,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
'No DESTINATION_DIR has been set. Set "Destination Directory" to the parent directory you want to move the files to')
albumpaths = [albumpath]
elif headphones.CONFIG.MOVE_FILES and headphones.CONFIG.DESTINATION_DIR:
- albumpaths = moveFiles(albumpath, release, tracks)
+ albumpaths = moveFiles(albumpath, release, metadata_dict)
else:
albumpaths = [albumpath]
@@ -454,10 +454,10 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
hash = seed_snatched['FolderName']
torrent_removed = False
logger.info(u'%s - %s. Checking if torrent has finished seeding and can be removed' % (
- release['ArtistName'], release['AlbumTitle']))
+ release['ArtistName'], release['AlbumTitle']))
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
torrent_removed = transmission.removeTorrent(hash, True)
- elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
+ elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
torrent_removed = deluge.removeTorrent(hash, True)
else:
torrent_removed = utorrent.removeTorrent(hash, True)
@@ -602,31 +602,24 @@ def embedAlbumArt(artwork, downloaded_track_list):
f.save()
except Exception as e:
logger.error(u'Error embedding album art to: %s. Error: %s' % (
- downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e)))
+ downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e)))
continue
-def addAlbumArt(artwork, albumpath, release):
+def addAlbumArt(artwork, albumpath, release, metadata_dict):
logger.info('Adding album art to folder')
+ md = metadata.album_metadata(albumpath, release, metadata_dict)
- try:
- year = release['ReleaseDate'][:4]
- except TypeError:
- year = ''
+ ext = ".jpg"
+ # PNGs are possibe here too
+ if artwork[:4] == '\x89PNG':
+ ext = ".png"
- values = {'$Artist': release['ArtistName'],
- '$Album': release['AlbumTitle'],
- '$Year': year,
- '$artist': release['ArtistName'].lower(),
- '$album': release['AlbumTitle'].lower(),
- '$year': year
- }
+ album_art_name = helpers.replace_all(
+ headphones.CONFIG.ALBUM_ART_FORMAT.strip(), md) + ext
- album_art_name = helpers.replace_all(headphones.CONFIG.ALBUM_ART_FORMAT.strip(),
- values) + ".jpg"
-
- album_art_name = helpers.replace_illegal_chars(album_art_name).encode(headphones.SYS_ENCODING,
- 'replace')
+ album_art_name = helpers.replace_illegal_chars(album_art_name).encode(
+ headphones.SYS_ENCODING, 'replace')
if headphones.CONFIG.FILE_UNDERSCORES:
album_art_name = album_art_name.replace(' ', '_')
@@ -653,7 +646,7 @@ def cleanupFiles(albumpath):
os.remove(os.path.join(r, files))
except Exception as e:
logger.error(u'Could not remove file: %s. Error: %s' % (
- files.decode(headphones.SYS_ENCODING, 'replace'), e))
+ files.decode(headphones.SYS_ENCODING, 'replace'), e))
def renameNFO(albumpath):
@@ -662,68 +655,27 @@ def renameNFO(albumpath):
for r, d, f in os.walk(albumpath):
for file in f:
if file.lower().endswith('.nfo'):
- logger.debug('Renaming: "%s" to "%s"' % (
- file.decode(headphones.SYS_ENCODING, 'replace'),
- file.decode(headphones.SYS_ENCODING, 'replace') + '-orig'))
- try:
- new_file_name = os.path.join(r, file)[:-3] + 'orig.nfo'
- os.rename(os.path.join(r, file), new_file_name)
- except Exception as e:
- logger.error(u'Could not rename file: %s. Error: %s' % (
- os.path.join(r, file).decode(headphones.SYS_ENCODING, 'replace'), e))
+ if not file.lower().endswith('.orig.nfo'):
+ logger.debug('Renaming: "%s" to "%s"' % (
+ file.decode(headphones.SYS_ENCODING, 'replace'),
+ file.decode(headphones.SYS_ENCODING, 'replace') + '-orig'))
+ try:
+ new_file_name = os.path.join(r, file)[:-3] + 'orig.nfo'
+ os.rename(os.path.join(r, file), new_file_name)
+ except Exception as e:
+ logger.error(u'Could not rename file: %s. Error: %s' % (
+ os.path.join(r, file).decode(headphones.SYS_ENCODING, 'replace'), e))
-def moveFiles(albumpath, release, tracks):
+def moveFiles(albumpath, release, metadata_dict):
logger.info("Moving files: %s" % albumpath)
- try:
- date = release['ReleaseDate']
- except TypeError:
- date = u''
- year = date[:4]
- artist = release['ArtistName'].replace('/', '_')
- album = release['AlbumTitle'].replace('/', '_')
+
+ md = metadata.album_metadata(albumpath, release, metadata_dict)
+ folder = helpers.replace_all(
+ headphones.CONFIG.FOLDER_FORMAT.strip(), md, normalize=True)
+
if headphones.CONFIG.FILE_UNDERSCORES:
- artist = artist.replace(' ', '_')
- album = album.replace(' ', '_')
-
- releasetype = release['Type'].replace('/', '_')
-
- if release['ArtistName'].startswith('The '):
- sortname = release['ArtistName'][4:] + ", The"
- else:
- sortname = release['ArtistName']
-
- if sortname[0].isdigit():
- firstchar = u'0-9'
- else:
- firstchar = sortname[0]
-
- for r, d, f in os.walk(albumpath):
- try:
- origfolder = os.path.basename(
- os.path.normpath(r).decode(headphones.SYS_ENCODING, 'replace'))
- except:
- origfolder = u''
-
- values = {'$Artist': artist,
- '$SortArtist': sortname,
- '$Album': album,
- '$Year': year,
- '$Date': date,
- '$Type': releasetype,
- '$OriginalFolder': origfolder,
- '$First': firstchar.upper(),
- '$artist': artist.lower(),
- '$sortartist': sortname.lower(),
- '$album': album.lower(),
- '$year': year,
- '$date': date,
- '$type': releasetype.lower(),
- '$first': firstchar.lower(),
- '$originalfolder': origfolder.lower()
- }
-
- folder = helpers.replace_all(headphones.CONFIG.FOLDER_FORMAT.strip(), values, normalize=True)
+ folder = folder.replace(' ', '_')
folder = helpers.replace_illegal_chars(folder, type="folder")
folder = folder.replace('./', '_/').replace('/.', '/_')
@@ -784,7 +736,7 @@ def moveFiles(albumpath, release, tracks):
except Exception as e:
logger.error(
"Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (
- lossless_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
+ lossless_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
create_duplicate_folder = True
if not headphones.CONFIG.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
@@ -807,7 +759,7 @@ def moveFiles(albumpath, release, tracks):
os.makedirs(lossless_destination_path)
except Exception as e:
logger.error('Could not create lossless folder for %s. (Error: %s)' % (
- release['AlbumTitle'], e))
+ release['AlbumTitle'], e))
if not make_lossy_folder:
return [albumpath]
@@ -822,7 +774,7 @@ def moveFiles(albumpath, release, tracks):
except Exception as e:
logger.error(
"Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (
- lossy_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
+ lossy_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
create_duplicate_folder = True
if not headphones.CONFIG.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
@@ -1068,78 +1020,25 @@ def embedLyrics(downloaded_track_list):
def renameFiles(albumpath, downloaded_track_list, release):
logger.info('Renaming files')
- try:
- date = release['ReleaseDate']
- except TypeError:
- date = u''
- year = date[:4]
-
# Until tagging works better I'm going to rely on the already provided metadata
for downloaded_track in downloaded_track_list:
- try:
- f = MediaFile(downloaded_track)
- except:
- logger.info("MediaFile couldn't parse: %s",
- downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
+ md, from_metadata = metadata.file_metadata(downloaded_track, release)
+ if md is None:
+ # unable to parse media file, skip file
continue
- if not f.disc:
- discnumber = ''
- else:
- discnumber = '%d' % f.disc
-
- if not f.track:
- tracknumber = ''
- else:
- tracknumber = '%02d' % f.track
-
- if not f.title:
-
- basename = os.path.basename(downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
- title = os.path.splitext(basename)[0]
- ext = os.path.splitext(basename)[1]
-
+ ext = md[metadata.Vars.EXTENSION]
+ if not from_metadata:
+ title = md[metadata.Vars.TITLE]
new_file_name = helpers.cleanTitle(title) + ext
-
else:
- title = f.title
+ new_file_name = helpers.replace_all(
+ headphones.CONFIG.FILE_FORMAT.strip(), md
+ ).replace('/', '_') + ext
- if release['ArtistName'] == "Various Artists" and f.artist:
- artistname = f.artist
- else:
- artistname = release['ArtistName']
-
- if artistname.startswith('The '):
- sortname = artistname[4:] + ", The"
- else:
- sortname = artistname
-
- values = {'$Disc': discnumber,
- '$Track': tracknumber,
- '$Title': title,
- '$Artist': artistname,
- '$SortArtist': sortname,
- '$Album': release['AlbumTitle'],
- '$Year': year,
- '$Date': date,
- '$disc': discnumber,
- '$track': tracknumber,
- '$title': title.lower(),
- '$artist': artistname.lower(),
- '$sortartist': sortname.lower(),
- '$album': release['AlbumTitle'].lower(),
- '$year': year,
- '$date': date
- }
-
- ext = os.path.splitext(downloaded_track)[1]
-
- new_file_name = helpers.replace_all(headphones.CONFIG.FILE_FORMAT.strip(),
- values).replace('/', '_') + ext
-
- new_file_name = helpers.replace_illegal_chars(new_file_name).encode(headphones.SYS_ENCODING,
- 'replace')
+ new_file_name = helpers.replace_illegal_chars(new_file_name).encode(
+ headphones.SYS_ENCODING, 'replace')
if headphones.CONFIG.FILE_UNDERSCORES:
new_file_name = new_file_name.replace(' ', '_')
@@ -1150,8 +1049,8 @@ def renameFiles(albumpath, downloaded_track_list, release):
new_file = os.path.join(albumpath, new_file_name)
if downloaded_track == new_file_name:
- logger.debug("Renaming for: " + downloaded_track.decode(headphones.SYS_ENCODING,
- 'replace') + " is not neccessary")
+ logger.debug("Renaming for: " + downloaded_track.decode(
+ headphones.SYS_ENCODING, 'replace') + " is not neccessary")
continue
logger.debug('Renaming %s ---> %s',
@@ -1277,7 +1176,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
if snatched:
if headphones.CONFIG.KEEP_TORRENT_FILES and snatched['Kind'] == 'torrent' and snatched[
- 'Status'] == 'Processed':
+ 'Status'] == 'Processed':
logger.info(
'%s is a torrent folder being preserved for seeding and has already been processed. Skipping.',
folder_basename)
@@ -1420,7 +1319,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
logger.info('No match found on MusicBrainz for: %s - %s', name, album)
# Fail here
- logger.info("Couldn't parse '%s' into any valid format. If adding " \
- "albums from another source, they must be in an 'Artist - Album " \
+ logger.info("Couldn't parse '%s' into any valid format. If adding "
+ "albums from another source, they must be in an 'Artist - Album "
"[Year]' format, or end with the musicbrainz release group id.",
folder_basename)
diff --git a/headphones/request.py b/headphones/request.py
index e29ce794..a4a9b128 100644
--- a/headphones/request.py
+++ b/headphones/request.py
@@ -55,7 +55,7 @@ def request_response(url, method="get", auto_raise=True,
# pose a security issue!
kwargs["verify"] = bool(headphones.CONFIG.VERIFY_SSL_CERT)
- #This fix is put in place for systems with broken SSL (like QNAP)
+ # This fix is put in place for systems with broken SSL (like QNAP)
if not headphones.CONFIG.VERIFY_SSL_CERT and sys.version_info >= (2, 7, 9):
try:
import ssl
diff --git a/headphones/rutracker.py b/headphones/rutracker.py
index 8395fbf3..af8e947e 100644
--- a/headphones/rutracker.py
+++ b/headphones/rutracker.py
@@ -3,10 +3,11 @@
import urllib
import time
from urlparse import urlparse
-
import re
+
import requests as requests
from bs4 import BeautifulSoup
+
import headphones
from headphones import logger
@@ -69,7 +70,6 @@ class Rutracker(object):
"""
Return the search url
"""
-
# Build search url
searchterm = ''
if artist != 'Various Artists':
@@ -91,22 +91,17 @@ class Rutracker(object):
# sort by size, descending.
sort = '&o=7&s=2'
-
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
-
logger.info("Searching rutracker using term: %s", searchterm)
-
return searchurl
def search(self, searchurl):
"""
Parse the search results and return valid torrent list
"""
-
try:
headers = {'Referer': self.search_referer}
r = self.session.get(url=searchurl, headers=headers, timeout=self.timeout)
-
soup = BeautifulSoup(r.content, 'html5lib')
# Debug
@@ -155,9 +150,8 @@ class Rutracker(object):
topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id
rulist.append((title, size, topicurl, 'rutracker.org', 'torrent', True))
else:
- logger.info(
- "%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)" % (
- title, size, int(seeds)))
+ logger.info("%s is larger than the maxsize or has too little seeders for this category, "
+ "skipping. (Size: %i bytes, Seeders: %i)" % (title, size, int(seeds)))
if not rulist:
logger.info("No valid results found from rutracker")
@@ -172,7 +166,6 @@ class Rutracker(object):
"""
return the .torrent data
"""
-
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
downloadurl = 'http://dl.rutracker.org/forum/dl.php?t=' + torrent_id
cookie = {'bb_dl': torrent_id}
@@ -187,7 +180,6 @@ class Rutracker(object):
# TODO get this working in utorrent.py
def utorrent_add_file(self, data):
-
host = headphones.CONFIG.UTORRENT_HOST
if not host.startswith('http'):
host = 'http://' + host
@@ -197,10 +189,8 @@ class Rutracker(object):
host = host[:-4]
base_url = host
-
url = base_url + '/gui/'
- self.session.auth = (
- headphones.CONFIG.UTORRENT_USERNAME, headphones.CONFIG.UTORRENT_PASSWORD)
+ self.session.auth = (headphones.CONFIG.UTORRENT_USERNAME, headphones.CONFIG.UTORRENT_PASSWORD)
try:
r = self.session.get(url + 'token.html')
diff --git a/headphones/searcher.py b/headphones/searcher.py
index 5c8b4144..6f71db52 100644
--- a/headphones/searcher.py
+++ b/headphones/searcher.py
@@ -119,10 +119,10 @@ def read_torrent_name(torrent_file, default_name=None):
return torrent_info["info"]["name"]
except KeyError:
if default_name:
- logger.warning("Couldn't get name from torrent file: %s. " \
+ logger.warning("Couldn't get name from torrent file: %s. "
"Defaulting to '%s'", e, default_name)
else:
- logger.warning("Couldn't get name from torrent file: %s. No " \
+ logger.warning("Couldn't get name from torrent file: %s. No "
"default given", e)
# Return default
@@ -143,7 +143,7 @@ def calculate_torrent_hash(link, data=None):
info = bdecode(data)["info"]
torrent_hash = sha1(bencode(info)).hexdigest()
else:
- raise ValueError("Cannot calculate torrent hash without magnet link " \
+ raise ValueError("Cannot calculate torrent hash without magnet link "
"or data")
return torrent_hash.upper()
@@ -208,7 +208,7 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
if release_date > datetime.datetime.today():
logger.info("Skipping: %s. Waiting for release date of: %s" % (
- album['AlbumTitle'], album['ReleaseDate']))
+ album['AlbumTitle'], album['ReleaseDate']))
continue
new = True
@@ -217,7 +217,7 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
losslessOnly = True
logger.info('Searching for "%s - %s" since it is marked as wanted' % (
- album['ArtistName'], album['AlbumTitle']))
+ album['ArtistName'], album['AlbumTitle']))
do_sorted_search(album, new, losslessOnly)
elif albumid and choose_specific_download:
@@ -229,24 +229,36 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
else:
album = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
logger.info('Searching for "%s - %s" since it was marked as wanted' % (
- album['ArtistName'], album['AlbumTitle']))
+ album['ArtistName'], album['AlbumTitle']))
do_sorted_search(album, new, losslessOnly)
logger.info('Search for wanted albums complete')
def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
- NZB_PROVIDERS = (
- headphones.CONFIG.HEADPHONES_INDEXER or headphones.CONFIG.NEWZNAB or headphones.CONFIG.NZBSORG or headphones.CONFIG.OMGWTFNZBS)
- NZB_DOWNLOADERS = (
- headphones.CONFIG.SAB_HOST or headphones.CONFIG.BLACKHOLE_DIR or headphones.CONFIG.NZBGET_HOST)
- TORRENT_PROVIDERS = (
- headphones.CONFIG.TORZNAB or headphones.CONFIG.KAT or headphones.CONFIG.PIRATEBAY or headphones.CONFIG.OLDPIRATEBAY or headphones.CONFIG.MININOVA or headphones.CONFIG.WAFFLES or headphones.CONFIG.RUTRACKER or headphones.CONFIG.WHATCD or headphones.CONFIG.STRIKE)
+ NZB_PROVIDERS = (headphones.CONFIG.HEADPHONES_INDEXER or
+ headphones.CONFIG.NEWZNAB or
+ headphones.CONFIG.NZBSORG or
+ headphones.CONFIG.OMGWTFNZBS)
+
+ NZB_DOWNLOADERS = (headphones.CONFIG.SAB_HOST or
+ headphones.CONFIG.BLACKHOLE_DIR or
+ headphones.CONFIG.NZBGET_HOST)
+
+ TORRENT_PROVIDERS = (headphones.CONFIG.TORZNAB or
+ headphones.CONFIG.KAT or
+ headphones.CONFIG.PIRATEBAY or
+ headphones.CONFIG.OLDPIRATEBAY or
+ headphones.CONFIG.MININOVA or
+ headphones.CONFIG.WAFFLES or
+ headphones.CONFIG.RUTRACKER or
+ headphones.CONFIG.WHATCD or
+ headphones.CONFIG.STRIKE)
results = []
myDB = db.DBConnection()
- albumlength = \
- myDB.select('SELECT sum(TrackDuration) from tracks WHERE AlbumID=?', [album['AlbumID']])[0][0]
+ albumlength = myDB.select('SELECT sum(TrackDuration) from tracks WHERE AlbumID=?',
+ [album['AlbumID']])[0][0]
if headphones.CONFIG.PREFER_TORRENTS == 0 and not choose_specific_download:
@@ -317,7 +329,7 @@ def more_filtering(results, album, albumlength, new):
# Lossless - ignore results if target size outside bitrate range
if headphones.CONFIG.PREFERRED_QUALITY == 3 and albumlength and (
- headphones.CONFIG.LOSSLESS_BITRATE_FROM or headphones.CONFIG.LOSSLESS_BITRATE_TO):
+ headphones.CONFIG.LOSSLESS_BITRATE_FROM or headphones.CONFIG.LOSSLESS_BITRATE_TO):
if headphones.CONFIG.LOSSLESS_BITRATE_FROM:
low_size_limit = albumlength / 1000 * int(headphones.CONFIG.LOSSLESS_BITRATE_FROM) * 128
if headphones.CONFIG.LOSSLESS_BITRATE_TO:
@@ -407,7 +419,7 @@ def sort_search_results(resultlist, album, new, albumlength):
if not targetsize:
logger.info('No track information for %s - %s. Defaulting to highest quality' % (
- album['ArtistName'], album['AlbumTitle']))
+ album['ArtistName'], album['AlbumTitle']))
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])),
reverse=True)
@@ -790,9 +802,9 @@ def send_to_downloader(data, bestqual, album):
return
else:
folder_name = '%s - %s [%s]' % (
- helpers.latinToAscii(album['ArtistName']).encode('UTF-8').replace('/', '_'),
- helpers.latinToAscii(album['AlbumTitle']).encode('UTF-8').replace('/', '_'),
- get_year_from_release_date(album['ReleaseDate']))
+ helpers.latinToAscii(album['ArtistName']).encode('UTF-8').replace('/', '_'),
+ helpers.latinToAscii(album['AlbumTitle']).encode('UTF-8').replace('/', '_'),
+ get_year_from_release_date(album['ReleaseDate']))
# Blackhole
if headphones.CONFIG.TORRENT_DOWNLOADER == 0:
@@ -842,16 +854,16 @@ def send_to_downloader(data, bestqual, album):
break
else:
# No service succeeded
- logger.warning("Unable to convert magnet with hash " \
+ logger.warning("Unable to convert magnet with hash "
"'%s' into a torrent file.", torrent_hash)
return
elif headphones.CONFIG.MAGNET_LINKS == 3:
torrent_to_file(download_path, data)
return
else:
- logger.error("Cannot save magnet link in blackhole. " \
- "Please switch your torrent downloader to " \
- "Transmission, uTorrent or Deluge, or allow Headphones " \
+ logger.error("Cannot save magnet link in blackhole. "
+ "Please switch your torrent downloader to "
+ "Transmission, uTorrent or Deluge, or allow Headphones "
"to open or convert magnet links")
return
else:
@@ -889,7 +901,7 @@ def send_to_downloader(data, bestqual, album):
if seed_ratio is not None:
transmission.setSeedRatio(torrentid, seed_ratio)
- elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
+ elif headphones.CONFIG.TORRENT_DOWNLOADER == 3: # Deluge
logger.info("Sending torrent to Deluge")
try:
@@ -917,10 +929,10 @@ def send_to_downloader(data, bestqual, album):
deluge.setSeedRatio({'hash': torrentid, 'ratio': seed_ratio})
# Set move-to directory
- if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
+ if headphones.CONFIG.DELUGE_DONE_DIRECTORY or headphones.CONFIG.DOWNLOAD_TORRENT_DIR:
deluge.setTorrentPath({'hash': torrentid})
- # I only just realized this function is useless...
+ # Get folder name from Deluge, it's usually the torrent name
folder_name = deluge.getTorrentFolder({'hash': torrentid})
if folder_name:
logger.info('Torrent folder name: %s' % folder_name)
@@ -1066,7 +1078,7 @@ def verifyresult(title, artistterm, term, lossless):
# Filter out FLAC if we're not specifically looking for it
if headphones.CONFIG.PREFERRED_QUALITY == (
- 0 or '0') and 'flac' in title.lower() and not lossless:
+ 0 or '0') and 'flac' in title.lower() and not lossless:
logger.info(
"Removed %s from results because it's a lossless album and we're not looking for a lossless album right now.",
title)
@@ -1463,7 +1475,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
except Exception as e:
gazelle = None
logger.error(u"What.cd credentials incorrect or site is down. Error: %s %s" % (
- e.__class__.__name__, str(e)))
+ e.__class__.__name__, str(e)))
if gazelle and gazelle.logged_in():
logger.info(u"Searching %s..." % provider)
@@ -1588,9 +1600,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
logger.info('Found %s. Size: %s' % (title, formatted_size))
else:
match = False
- logger.info(
- '%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (
- title, size, int(seeds)))
+ logger.info('%s is larger than the maxsize or has too little seeders for this category, '
+ 'skipping. (Size: %i bytes, Seeders: %i)' % (title, size, int(seeds)))
resultlist.append((title, size, url, provider, "torrent", match))
except Exception as e:
@@ -1644,9 +1655,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
logger.info('Found %s. Size: %s' % (title, formatted_size))
else:
match = False
- logger.info(
- '%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (
- title, size, int(seeds)))
+ logger.info('%s is larger than the maxsize or has too little seeders for this category, '
+ 'skipping. (Size: %i bytes, Seeders: %i)' % (title, size, int(seeds)))
resultlist.append((title, size, url, provider, "torrent", match))
except Exception as e:
@@ -1746,9 +1756,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
else:
match = False
- logger.info(
- '%s is larger than the maxsize, the wrong format or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i, Format: %s)' % (
- title, size, int(seeds), rightformat))
+ logger.info('%s is larger than the maxsize, the wrong format or has too little seeders'
+ ' for this category, skipping. (Size: %i bytes, Seeders: %i, Format: %s)' % (
+ title, size, int(seeds), rightformat))
resultlist.append((title, size, url, provider, 'torrent', match))
except Exception as e:
@@ -1757,8 +1767,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
# attempt to verify that this isn't a substring result
# when looking for "Foo - Foo" we don't want "Foobar"
# this should be less of an issue when it isn't a self-titled album so we'll only check vs artist
- results = [result for result in resultlist if
- verifyresult(result[0], artistterm, term, losslessOnly)]
+ results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)]
# Additional filtering for size etc
if results and not choose_specific_download:
@@ -1779,7 +1788,7 @@ def preprocess(resultlist):
return ruobj.get_torrent_data(result[2]), result
# Get out of here if we're using Transmission
- if headphones.CONFIG.TORRENT_DOWNLOADER == 1: ## if not a magnet link still need the .torrent to generate hash... uTorrent support labeling
+ if headphones.CONFIG.TORRENT_DOWNLOADER == 1: # if not a magnet link still need the .torrent to generate hash... uTorrent support labeling
return True, result
# Get out of here if it's a magnet link
if result[2].lower().startswith("magnet:"):
@@ -1806,7 +1815,7 @@ def preprocess(resultlist):
headers = {'User-Agent': USER_AGENT}
if result[3] == 'headphones':
- return request.request_content(url=result[2], headers=headers, auth=(
- headphones.CONFIG.HPUSER, headphones.CONFIG.HPPASS)), result
+ return request.request_content(url=result[2], headers=headers,
+ auth=(headphones.CONFIG.HPUSER, headphones.CONFIG.HPPASS)), result
else:
return request.request_content(url=result[2], headers=headers), result
diff --git a/headphones/softchroot.py b/headphones/softchroot.py
index 3716a035..4f689e10 100644
--- a/headphones/softchroot.py
+++ b/headphones/softchroot.py
@@ -1,4 +1,5 @@
import os
+
from headphones.exceptions import SoftChrootError
@@ -13,7 +14,7 @@ class SoftChroot(object):
def __init__(self, path):
if not path:
- #disabled
+ # disabled
return
path = path.strip()
diff --git a/headphones/softchroot_test.py b/headphones/softchroot_test.py
index 474969d7..b310ce12 100644
--- a/headphones/softchroot_test.py
+++ b/headphones/softchroot_test.py
@@ -1,7 +1,6 @@
import os
import mock
from headphones.unittestcompat import TestCase, TestArgs
-#from mock import MagicMock
from headphones.softchroot import SoftChroot
from headphones.exceptions import SoftChrootError
diff --git a/headphones/transmission.py b/headphones/transmission.py
index b62e2fac..3e436617 100644
--- a/headphones/transmission.py
+++ b/headphones/transmission.py
@@ -164,7 +164,7 @@ def torrentAction(method, arguments):
whitelist_status_code=[401, 409])
if response.status_code == 401:
if auth:
- logger.error("Username and/or password not accepted by " \
+ logger.error("Username and/or password not accepted by "
"Transmission")
else:
logger.error("Transmission authorization required")
diff --git a/headphones/versioncheck.py b/headphones/versioncheck.py
index 30098886..341f7926 100644
--- a/headphones/versioncheck.py
+++ b/headphones/versioncheck.py
@@ -124,7 +124,7 @@ def checkGithub():
# Get the latest version available from github
logger.info('Retrieving latest version information from GitHub')
url = 'https://api.github.com/repos/%s/headphones/commits/%s' % (
- headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
+ headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict)
if version is None:
@@ -147,7 +147,7 @@ def checkGithub():
logger.info('Comparing currently installed version with latest GitHub version')
url = 'https://api.github.com/repos/%s/headphones/compare/%s...%s' % (
- headphones.CONFIG.GIT_USER, headphones.LATEST_VERSION, headphones.CURRENT_VERSION)
+ headphones.CONFIG.GIT_USER, headphones.LATEST_VERSION, headphones.CURRENT_VERSION)
commits = request.request_json(url, timeout=20, whitelist_status_code=404,
validator=lambda x: type(x) == dict)
@@ -192,7 +192,7 @@ def update():
else:
tar_download_url = 'https://github.com/%s/headphones/tarball/%s' % (
- headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
+ headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
update_dir = os.path.join(headphones.PROG_DIR, 'update')
version_path = os.path.join(headphones.PROG_DIR, 'version.txt')
diff --git a/headphones/webserve.py b/headphones/webserve.py
index 741a37f4..f4a06c2b 100644
--- a/headphones/webserve.py
+++ b/headphones/webserve.py
@@ -29,7 +29,7 @@ import urllib2
import os
import re
from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers
-from headphones.helpers import checked, radio, today, cleanName
+from headphones.helpers import checked, radio, today, clean_name
from mako.lookup import TemplateLookup
from mako import exceptions
import headphones
@@ -281,8 +281,7 @@ class WebInterface(object):
def scanArtist(self, ArtistID):
myDB = db.DBConnection()
- artist_name = \
- myDB.select('SELECT DISTINCT ArtistName FROM artists WHERE ArtistID=?', [ArtistID])[0][0]
+ artist_name = myDB.select('SELECT DISTINCT ArtistName FROM artists WHERE ArtistID=?', [ArtistID])[0][0]
logger.info(u"Scanning artist: %s", artist_name)
@@ -292,8 +291,7 @@ class WebInterface(object):
acceptable_formats = ["$artist", "$sortartist", "$first/$artist", "$first/$sortartist"]
if not folder_format.lower() in acceptable_formats:
- logger.info(
- "Can't determine the artist folder from the configured folder_format. Not scanning")
+ logger.info("Can't determine the artist folder from the configured folder_format. Not scanning")
return
# Format the folder to match the settings
@@ -388,8 +386,7 @@ class WebInterface(object):
if ArtistID:
ArtistIDT = ArtistID
else:
- ArtistIDT = \
- myDB.action('SELECT ArtistID FROM albums WHERE AlbumID=?', [mbid]).fetchone()[0]
+ ArtistIDT = myDB.action('SELECT ArtistID FROM albums WHERE AlbumID=?', [mbid]).fetchone()[0]
myDB.action(
'UPDATE artists SET TotalTracks=(SELECT COUNT(*) FROM tracks WHERE ArtistID = ? AND AlbumTitle IN (SELECT AlbumTitle FROM albums WHERE Status != "Ignored")) WHERE ArtistID = ?',
[ArtistIDT, ArtistIDT])
@@ -577,7 +574,7 @@ class WebInterface(object):
for albums in have_albums:
# Have to skip over manually matched tracks
if albums['ArtistName'] and albums['AlbumTitle'] and albums['TrackTitle']:
- original_clean = helpers.cleanName(
+ original_clean = helpers.clean_name(
albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle'])
# else:
# original_clean = None
@@ -595,10 +592,12 @@ class WebInterface(object):
# unmatchedalbums = [f for f in have_album_dictionary if f not in [x for x in headphones_album_dictionary]]
check = set(
- [(cleanName(d['ArtistName']).lower(), cleanName(d['AlbumTitle']).lower()) for d in
+ [(clean_name(d['ArtistName']).lower(),
+ clean_name(d['AlbumTitle']).lower()) for d in
headphones_album_dictionary])
unmatchedalbums = [d for d in have_album_dictionary if (
- cleanName(d['ArtistName']).lower(), cleanName(d['AlbumTitle']).lower()) not in check]
+ clean_name(d['ArtistName']).lower(),
+ clean_name(d['AlbumTitle']).lower()) not in check]
return serve_template(templatename="manageunmatched.html", title="Manage Unmatched Items",
unmatchedalbums=unmatchedalbums)
@@ -622,8 +621,8 @@ class WebInterface(object):
(artist, album))
elif action == "matchArtist":
- existing_artist_clean = helpers.cleanName(existing_artist).lower()
- new_artist_clean = helpers.cleanName(new_artist).lower()
+ existing_artist_clean = helpers.clean_name(existing_artist).lower()
+ new_artist_clean = helpers.clean_name(new_artist).lower()
if new_artist_clean != existing_artist_clean:
have_tracks = myDB.action(
'SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=?',
@@ -659,8 +658,7 @@ class WebInterface(object):
# This was throwing errors and I don't know why, but it seems to be working fine.
# else:
# logger.info("There was an error modifying Artist %s. This should not have happened" % existing_artist)
- logger.info("Manual matching yielded %s new matches for Artist: %s" % (
- update_count, new_artist))
+ logger.info("Manual matching yielded %s new matches for Artist: %s" % (update_count, new_artist))
if update_count > 0:
librarysync.update_album_status()
else:
@@ -668,10 +666,10 @@ class WebInterface(object):
"Artist %s already named appropriately; nothing to modify" % existing_artist)
elif action == "matchAlbum":
- existing_artist_clean = helpers.cleanName(existing_artist).lower()
- new_artist_clean = helpers.cleanName(new_artist).lower()
- existing_album_clean = helpers.cleanName(existing_album).lower()
- new_album_clean = helpers.cleanName(new_album).lower()
+ existing_artist_clean = helpers.clean_name(existing_artist).lower()
+ new_artist_clean = helpers.clean_name(new_artist).lower()
+ existing_album_clean = helpers.clean_name(existing_album).lower()
+ new_album_clean = helpers.clean_name(new_album).lower()
existing_clean_string = existing_artist_clean + " " + existing_album_clean
new_clean_string = new_artist_clean + " " + new_album_clean
if existing_clean_string != new_clean_string:
@@ -712,13 +710,13 @@ class WebInterface(object):
# else:
# logger.info("There was an error modifying Artist %s / Album %s with clean name %s" % (existing_artist, existing_album, existing_clean_string))
logger.info("Manual matching yielded %s new matches for Artist: %s / Album: %s" % (
- update_count, new_artist, new_album))
+ update_count, new_artist, new_album))
if update_count > 0:
librarysync.update_album_status(album_id)
else:
logger.info(
"Artist %s / Album %s already named appropriately; nothing to modify" % (
- existing_artist, existing_album))
+ existing_artist, existing_album))
@cherrypy.expose
def manageManual(self):
@@ -728,10 +726,10 @@ class WebInterface(object):
'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have')
for albums in manualalbums:
if albums['ArtistName'] and albums['AlbumTitle'] and albums['TrackTitle']:
- original_clean = helpers.cleanName(
+ original_clean = helpers.clean_name(
albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle'])
if albums['Matched'] == "Ignored" or albums['Matched'] == "Manual" or albums[
- 'CleanName'] != original_clean:
+ 'CleanName'] != original_clean:
if albums['Matched'] == "Ignored":
album_status = "Ignored"
elif albums['Matched'] == "Manual" or albums['CleanName'] != original_clean:
@@ -769,7 +767,7 @@ class WebInterface(object):
[artist])
update_count = 0
for tracks in update_clean:
- original_clean = helpers.cleanName(
+ original_clean = helpers.clean_name(
tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks[
'TrackTitle']).lower()
album = tracks['AlbumTitle']
@@ -797,7 +795,7 @@ class WebInterface(object):
(artist, album))
update_count = 0
for tracks in update_clean:
- original_clean = helpers.cleanName(
+ original_clean = helpers.clean_name(
tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks[
'TrackTitle']).lower()
track_title = tracks['TrackTitle']
@@ -990,14 +988,14 @@ class WebInterface(object):
totalcount = len(filtered)
else:
query = 'SELECT * from artists WHERE ArtistSortName LIKE "%' + sSearch + '%" OR LatestAlbum LIKE "%' + sSearch + '%"' + 'ORDER BY %s COLLATE NOCASE %s' % (
- sortcolumn, sSortDir_0)
+ sortcolumn, sSortDir_0)
filtered = myDB.select(query)
totalcount = myDB.select('SELECT COUNT(*) from artists')[0][0]
if sortbyhavepercent:
filtered.sort(key=lambda x: (
- float(x['HaveTracks']) / x['TotalTracks'] if x['TotalTracks'] > 0 else 0.0,
- x['HaveTracks'] if x['HaveTracks'] else 0.0), reverse=sSortDir_0 == "asc")
+ float(x['HaveTracks']) / x['TotalTracks'] if x['TotalTracks'] > 0 else 0.0,
+ x['HaveTracks'] if x['HaveTracks'] else 0.0), reverse=sSortDir_0 == "asc")
# can't figure out how to change the datatables default sorting order when its using an ajax datasource so ill
# just reverse it here and the first click on the "Latest Album" header will sort by descending release date
@@ -1157,6 +1155,7 @@ class WebInterface(object):
"transmission_username": headphones.CONFIG.TRANSMISSION_USERNAME,
"transmission_password": headphones.CONFIG.TRANSMISSION_PASSWORD,
"deluge_host": headphones.CONFIG.DELUGE_HOST,
+ "deluge_cert": headphones.CONFIG.DELUGE_CERT,
"deluge_password": headphones.CONFIG.DELUGE_PASSWORD,
"deluge_label": headphones.CONFIG.DELUGE_LABEL,
"deluge_done_directory": headphones.CONFIG.DELUGE_DONE_DIRECTORY,
@@ -1462,7 +1461,6 @@ class WebInterface(object):
continue
conftype = _conf[1]
- #print '===>', conftype
if conftype is headphones.config.path:
nv = headphones.SOFT_CHROOT.revoke(v)
if nv != v:
diff --git a/headphones/webstart.py b/headphones/webstart.py
index a4ab7113..61417803 100644
--- a/headphones/webstart.py
+++ b/headphones/webstart.py
@@ -33,14 +33,14 @@ def initialize(options):
# If either the HTTPS certificate or key do not exist, try to make
# self-signed ones.
if not (https_cert and os.path.exists(https_cert)) or not (
- https_key and os.path.exists(https_key)):
+ https_key and os.path.exists(https_key)):
if not create_https_certificates(https_cert, https_key):
- logger.warn("Unable to create certificate and key. Disabling " \
+ logger.warn("Unable to create certificate and key. Disabling "
"HTTPS")
enable_https = False
if not (os.path.exists(https_cert) and os.path.exists(https_key)):
- logger.warn("Disabled HTTPS because of missing certificate and " \
+ logger.warn("Disabled HTTPS because of missing certificate and "
"key.")
enable_https = False
diff --git a/lib/unittestcompat.py b/lib/unittestcompat.py
index 12a497be..e30832d1 100644
--- a/lib/unittestcompat.py
+++ b/lib/unittestcompat.py
@@ -45,6 +45,10 @@ class TestCase(TC):
def assertRegexpMatches(self, *args, **kw):
return super(TestCase, self).assertRegexpMatches(*args, **kw)
+ @_d
+ def assertItemsEqual(self, *args, **kw):
+ return super(TestCase, self).assertItemsEqual(*args, **kw)
+
# -----------------------------------------------------------
# NOT DUMMY ASSERTIONS
# -----------------------------------------------------------
@@ -107,7 +111,8 @@ def TestArgs(*parameters):
name_for_parameter = method.__name__ + "(" + args_for_parameter + ")"
frame = sys._getframe(1) # pylint: disable-msg=W0212
frame.f_locals[name_for_parameter] = method_for_parameter
- frame.f_locals[name_for_parameter].__doc__ = method.__doc__ + '(' + args_for_parameter + ')'
+ if method.__doc__:
+ frame.f_locals[name_for_parameter].__doc__ = method.__doc__ + '(' + args_for_parameter + ')'
method_for_parameter.__name__ = name_for_parameter + '(' + args_for_parameter + ')'
return None
return decorator
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 00000000..09a50e63
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,8 @@
+coverage==4.0.3
+coveralls==1.1
+mock==1.3.0
+nose==1.3.7
+pep8==1.7.0
+pyflakes==1.1.0
+pylint==1.3.1 # pylint 1.4 does not run under python 2.6
+pyOpenSSL==0.15.1