Compare commits

...
170 Commits
Author SHA1 Message Date
Ruud 3b7376fd18 One up 2013-07-06 01:01:26 +02:00
Ruud c31b10c798 Ignore current suggested results 2013-07-06 00:49:11 +02:00
Ruud acda664686 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2013-07-05 22:43:54 +02:00
Ruud 99123ad1c3 Remove version on branch 2013-07-05 22:17:43 +02:00
Ruud cdf9cf5cf4 Yifi: don't search empty results. fix #1900 2013-07-05 21:54:55 +02:00
Ruud 797dedfcbb Remove cdX from subname. fix #1524 2013-07-05 21:28:07 +02:00
Ruud b61de4866c Make subliminal work with Requests 1.0+ 2013-07-05 20:40:27 +02:00
Ruud 931951ff37 Change default min size for 720p and 1080p 2013-06-30 15:57:58 +02:00
Ruud 6f42b4c316 Don't show coming soon when no dvd release is set 2013-06-30 15:21:06 +02:00
Ruud 58c446de2d Make string param boolean 2013-06-30 15:20:02 +02:00
Ruud 74bf6bc411 Always set info dict on library 2013-06-30 13:17:56 +02:00
Ruud ad3c24f950 Improved "too early to search" calculations 2013-06-30 13:17:43 +02:00
Ruud 93346b0c63 Properly update release dates 2013-06-30 01:16:13 +02:00
Ruud b1942678b4 Add hash and date to update available notification. fix #1883 2013-06-29 22:20:35 +02:00
Ruud 8c77d0d775 Add advanced option to search on launch. fix #1887 2013-06-29 22:03:29 +02:00
Ruud 3e667ee39a Couldn't press letter in movie filter. fix #1888 2013-06-29 21:56:24 +02:00
Ruud 52b2858ac2 Don't enable yifi by default 2013-06-29 21:39:53 +02:00
Ruud 6fcb4c2058 Change default automation interval 2013-06-29 21:07:07 +02:00
Ruud 2e8f670e94 Remove import 2013-06-28 23:32:38 +02:00
Ruud bd56539103 Yifi cleanup 2013-06-24 22:31:50 +02:00
Ruud 9bcd3de69b Merge branch 'develop' of git://github.com/Mochaka/CouchPotatoServer into Mochaka-develop 2013-06-24 22:08:00 +02:00
Ruud d8f57963a1 NZBIndex: Search for year inside brackets. closes #1874 2013-06-24 22:07:21 +02:00
Ruud 5328f7fe69 Allow unknown keywords for all api calls. fix #1881 2013-06-24 21:21:49 +02:00
Ruud 9eea42b121 Get array arguments as list. fix #1875 2013-06-24 00:26:00 +02:00
Ruud 374f8ba1de Allow non trailing slash API calls 2013-06-23 23:28:13 +02:00
Ruud 74c984dec3 Send CP headers to suggestion call. fix #1872 2013-06-23 20:44:11 +02:00
Ruud 52ea0215f0 Use done for suggestion also 2013-06-23 19:14:11 +02:00
Ruud ea3d719b32 Suggest on wrong dev port 2013-06-23 19:09:07 +02:00
Ruud fd1e655075 Initial suggestion support 2013-06-23 19:07:03 +02:00
Ruud 9f8d439780 Add limit to CP search api 2013-06-22 17:01:24 +02:00
Aaron Florey 7e1bdc99eb Add Yify Torrent Provider 2013-06-23 00:11:01 +10:00
Ruud dac36d7f55 IPTorrents ignore empty results 2013-06-22 14:16:02 +02:00
Ruud 9d495a10ec Unicode static folder 2013-06-22 01:38:07 +02:00
Ruud 9bb99319ba SplitString don't clean 2013-06-22 01:37:27 +02:00
Ruud bc8d8dcd04 Update guessit with unicode fix 2013-06-22 00:34:58 +02:00
Ruud b2d9a7675d Add version to SAB description 2013-06-22 00:33:12 +02:00
Ruud 2477197656 Don't use unicode in repo 2013-06-22 00:33:00 +02:00
Ruud 171083b2f1 Remove empty values from splitString. fix #1795 2013-06-21 13:44:14 +02:00
Ruud e592eb969f NZBget error when downloadrate is 0. fix #1849 2013-06-21 13:00:58 +02:00
Ruud db1493f138 Update pytwitter library. fix #1847 2013-06-21 12:50:58 +02:00
Ruud 57c270f8fa Don't break while sending messages to listeners 2013-06-21 11:32:45 +02:00
Ruud bfe8bc89c0 IMDB description csv link 2013-06-19 23:39:00 +02:00
Ruud 0a00862495 Show csv imdb export in image 2013-06-19 23:34:58 +02:00
sax 7dd53d93cd Added nzb support to Synology downloader. 2013-06-19 22:43:11 +02:00
theorem21 abe65d4064 Update README.md
added FreeBSD installation instructions.  Requires additional FreeBSD init script (pending creation)
2013-06-19 22:36:56 +02:00
Ruud 4977b31ba6 Use failed status to ignore releases too 2013-06-16 00:21:33 +02:00
Ruud c1beb85ba5 Add spotter to name for scoring 2013-06-15 23:32:44 +02:00
Ruud ca9a78eea4 Advanced option for XBMC to only update first in list
Thanks @cliffordwhansen
2013-06-15 22:17:23 +02:00
Ruud 9bf006f4d3 Return if api is not found 2013-06-15 21:43:14 +02:00
Ruud 3bb2a082b7 AwesomeHD provider
Thanks @jrsdead
2013-06-15 20:41:23 +02:00
Ruud 92d11522d2 Use id for HDBits torrent name 2013-06-15 00:06:14 +02:00
Ruud 44cfdc1503 Include full requests lib 2013-06-15 00:04:15 +02:00
Ruud 2fdcbedea8 Use has_key for events check 2013-06-15 00:02:37 +02:00
Ruud 787c7fd966 Codestyle cleanup 2013-06-14 23:35:28 +02:00
sax 09b4ad6937 Fixed torrent support for Synology downloader to work properly with torrent files passed directly by CouchPotato. 2013-06-14 23:31:56 +02:00
sax 580d43aeaf Updated requests library to version 1.2.3 2013-06-14 23:31:47 +02:00
sax a1a7fec15f Added torrent support for Synology downloader. 2013-06-14 23:31:40 +02:00
Ruud 6dcd74d116 Re-use code for ignore toggle 2013-06-14 23:21:41 +02:00
Ruud 187f5a8a93 Merge branch 'develop' of git://github.com/mano3m/CouchPotatoServer into mano3m-develop 2013-06-14 22:43:19 +02:00
Ruud 2eb938147a Move login downloads to default list item 2013-06-14 22:08:25 +02:00
Ruud deffb75c14 TorrentByte provider
Thanks @StealthGod
2013-06-14 21:52:15 +02:00
Ruud f91707bfbe Uncomment debug code 2013-06-14 21:51:41 +02:00
Ruud 8aba7825dc Only show to_early when it has items 2013-06-14 21:24:38 +02:00
Joel Kåberg b8b5b2fef2 dont spam the log damnit! 2013-06-14 21:17:45 +02:00
Ruud f4d6d69184 Check if handler has parent 2013-06-14 20:48:20 +02:00
Ben Fox-Moore a5b1c685e1 Allow IPTorrents provider to read results across multiple pages
Conflicts:
	couchpotato/core/providers/torrent/iptorrents/main.py
2013-06-14 20:47:05 +02:00
Ruud 609805b84d Don't allow keyerror in event 2013-06-14 20:04:49 +02:00
Ruud 00d1da7c01 Bind quickscan to class 2013-06-14 19:51:31 +02:00
Ruud 7335726c7d Add handler aswell 2013-06-14 19:51:20 +02:00
Ruud 02779939f0 Catch im_self error 2013-06-14 19:51:14 +02:00
Ruud 6c6f015f40 Use str not unicode in minification 2013-06-14 19:47:54 +02:00
Ruud f087d38b86 Cleanup 2013-06-14 17:36:26 +02:00
Ruud c78957f55c Don't try to run event without beforeCall 2013-06-14 17:24:34 +02:00
Ruud 9ce0c47cd4 More login fixes 2013-06-14 16:03:02 +02:00
Ruud c9a4af218e Send port with referer. fix #1827 2013-06-14 13:54:01 +02:00
Ruud c5c2e61e06 Log startup errors 2013-06-14 11:22:29 +02:00
Ruud b2930dd6a7 Encode used path on startup. fix #1797 fix #1297 2013-06-14 11:11:34 +02:00
Ruud 4aa6700ceb Update SQLAlchemy 2013-06-14 11:00:06 +02:00
Ruud 267ecfacab Status check in ubuntu init script
Thanks @LeonB
2013-06-14 08:57:37 +02:00
Ruud 5699abf1be Use new KickAss domain 2013-06-14 00:43:03 +02:00
Ruud a6ccd037e2 Login check for SceneHD 2013-06-14 00:37:55 +02:00
Ruud 009991ce4c Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2013-06-14 00:04:44 +02:00
Ruud 6ef788a8f4 Check login after 1 hour 2013-06-14 00:03:48 +02:00
Ruud fa37f7d40a Add some logging to core messaging 2013-06-13 12:15:59 +02:00
Ruud b195cebac7 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2013-06-12 23:46:35 +02:00
Ruud 8aeea60888 Update Tornado 2013-06-12 23:42:38 +02:00
Ruud 6e0857c6c1 Remove Flask dependencies 2013-06-12 23:37:08 +02:00
Ruud Burger 260fdbe3b3 Merge pull request #1836 from clinton-hall/develop-extra-logging
Add logging when no rating available
2013-06-12 02:05:21 -07:00
mano3m 2f30c6c781 fix failed issues
As reported in issue #1822 I broke try next release when failed. This
commit adds the failed status to several items.
2013-06-11 21:43:01 +02:00
Clinton Hall d5b4da655a add logging when no rating available 2013-06-11 21:51:56 +09:30
Ruud 1694ed7758 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2013-06-10 21:14:02 +02:00
Ruud ee6cc6d319 PTP torrent id in lowercases 2013-06-10 21:10:58 +02:00
Ruud Burger 7670e320ba Merge pull request #1799 from clinton-hall/develop-audiochannels
add audio channels to renamer
2013-06-10 11:39:34 -07:00
Ruud 15ab745bd0 Don't assume imdb key. fixes #1819 2013-06-08 18:04:46 +02:00
Ruud 7468b33991 Send along ignored movies 2013-06-08 17:32:45 +02:00
Ruud 750e02f38a Close zipfile. fixes #1798 2013-06-08 16:04:14 +02:00
Ruud e2852407ea One up 2013-06-03 22:22:44 +02:00
Ruud 88e738c6cd Don't show double updater name 2013-06-03 22:22:35 +02:00
Ruud eaae8bdb0b Merge branch 'refs/heads/develop' into desktop 2013-06-03 22:00:21 +02:00
Ruud 95d146fea2 Send referer with scheme 2013-06-02 14:22:59 +02:00
Ruud dc20b68a37 See if need to login on "belongs_to" check. fix #1190 2013-05-31 16:32:03 +02:00
clinton-hall 563e3072a5 add audio channels to renamer 2013-05-31 13:44:40 +09:30
Ruud b3ba4db00b Append instead of add for subtitle file list 2013-05-29 19:30:51 +02:00
Ruud a4c1480a1a Force update check from dropdown 2013-05-29 19:03:49 +02:00
Ruud 91e0452320 Torrentshack cleanup 2013-05-29 19:03:28 +02:00
Ruud ad80ea7885 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2013-05-29 18:33:36 +02:00
Ruud 1c20cda389 Set updater crons on start. 2013-05-29 14:50:22 +02:00
sax 631759d833 Added configuration option to search over scene releases only. Fixed release name issue (removed ­ element). 2013-05-28 22:52:22 +02:00
sax ca02c66f26 Fixed login success detection. 2013-05-28 22:52:13 +02:00
sax 3ac095d359 Added support for Torrent Shack provider. 2013-05-28 22:52:05 +02:00
Ruud e1bc223de0 Get year with default 2013-05-28 21:13:15 +02:00
Ruud e065ead9b3 Api on subdomain 2013-05-26 21:50:20 +02:00
Ruud f9471f9b9b HDBits cleanup 2013-05-26 15:16:55 +02:00
Ronald Pompa 2612b50d06 created hdbits torrent provider 2013-05-26 14:30:10 +02:00
Ruud d9ce2906a0 Fix line ending 2013-05-26 14:24:59 +02:00
Joel Kåberg b76397f98e addApiView explenation 2013-05-26 14:22:31 +02:00
Joel Kåberg fcad9e0be5 fireAsync made optional 2013-05-26 14:22:23 +02:00
Ruud 2934347865 Send user-agent on login 2013-05-26 14:20:05 +02:00
Ruud 315f1b0207 Add r6 to quality list 2013-05-23 07:35:11 +02:00
Ruud 965bd79a86 Cleanup import 2013-05-19 23:20:32 +02:00
Ruud c18563e34b uTorrent cleanup 2013-05-19 23:12:22 +02:00
Ruud 161e0de8d5 Don't need makedir in Transmission 2013-05-19 22:51:32 +02:00
Ruud 40aeca0740 PTP extra scoring 2013-05-19 22:18:01 +02:00
Ruud 63dd7fa7c0 New PTP-config for more accurate hits
Conflicts:
	couchpotato/core/providers/torrent/passthepopcorn/main.py
2013-05-19 22:10:51 +02:00
Ruud 509b49caf1 Deepcopy and merge movie info results 2013-05-19 01:12:09 +02:00
Ruud 38c51cf79c Import cleanup 2013-05-19 00:57:50 +02:00
Ruud 0b693bba4e Add "on snatch" options to XBMC & Plex notifications
fix #1379
2013-05-19 00:30:56 +02:00
Ruud 1258f34c78 Update counter on movie add / delete
fix #1383
2013-05-19 00:22:44 +02:00
Ruud 510c0d5f56 Remove size_check in quality guess
fix #1393
2013-05-19 00:12:19 +02:00
Ruud cdb630e580 More touch fixes 2013-05-18 23:59:37 +02:00
Ruud 65fbd38105 Make buttons more touch friendly
fix #1416
2013-05-18 23:37:49 +02:00
Ruud 1570132a55 Don't try to rss parse empty string
fix #1418
2013-05-18 22:52:02 +02:00
Ruud 7b5b748d23 Failed joining unicode and none unicode paths
fix #1447
2013-05-18 22:45:09 +02:00
Ruud 041601c4a5 Change TPB search string
fix #1451
2013-05-18 22:12:43 +02:00
Ruud f692fd0202 Make sure info isn't not overwriten by none
fix #1724
2013-05-18 21:53:17 +02:00
Ruud e7b4de56f2 Only run updater if enabled.
fix #1756
2013-05-18 20:30:48 +02:00
Ruud 4a616a0c04 Placeholder styling 2013-05-18 19:40:26 +02:00
Ruud 0814675d2a Remove prints in clientscript 2013-05-18 17:28:25 +02:00
Ruud 13df35462b Force expire database objects 2013-05-17 21:36:23 +02:00
Ruud 899868f51e Don't show empty message when search 2013-05-17 21:32:46 +02:00
Ruud ee466aebce Easily reset search 2013-05-17 18:32:20 +02:00
Ruud 687ef2662e Switch filter and view 2013-05-17 17:52:19 +02:00
Ruud 5aa29acbd3 Logging fixes 2013-05-17 17:51:15 +02:00
Ruud 1c2b3d063b Empty wanted list background 2013-05-17 15:40:27 +02:00
Ruud 551a000893 Incorrect marking as BD-Rip
Fixes #1643
2013-05-17 15:12:13 +02:00
Ruud 0d82d425cc Show original message when log is failing
closes #1735
2013-05-17 12:41:31 +02:00
Ruud 0e1cea1034 Simplify minifier
fixes #1744
2013-05-17 12:30:48 +02:00
Ruud 2b75153148 Don't limit snatched & wanted
fixes #1747
2013-05-17 12:11:53 +02:00
Ruud c170615fb3 Ignore temp updater files on cleanup 2013-05-15 14:49:45 +02:00
Ruud f6e84b6a35 Remove view after update 2013-05-14 00:22:19 +02:00
Ruud 6144f09a1f Make lists of sorted movies files also 2013-05-14 00:15:28 +02:00
Ruud de142e8050 Goodfilm fixes. closes #1723
Thanks @qooplmao
2013-05-14 00:13:42 +02:00
Ruud d0c1a119fd Use list for leftover files 2013-05-13 23:48:02 +02:00
Ruud 8fd80d3185 Update instead of extend 2013-05-13 23:44:01 +02:00
Ruud ae28c82858 Cleanup 2013-05-13 23:27:46 +02:00
Ruud 1766764c7d Skip available movies in "still not available" view. fix #1687 2013-05-13 23:07:57 +02:00
Ruud 129f8d72bd API movie.list didn't return proper total. fix #1727 2013-05-13 21:37:54 +02:00
Ruud 7314b5ecae Run async event in thread so the on_complete is fired properly 2013-05-11 00:04:59 +02:00
Ruud 7b0806355f Thumbnail list action position 2013-05-10 19:36:48 +02:00
Ruud 49cf72e058 Load notification after window load 2013-05-10 18:28:52 +02:00
Ruud a11cad619d Don't unicode css 2013-05-10 18:06:10 +02:00
Ruud c1d35e8a57 Stop blinking text when scrolling in webkit 2013-05-10 15:19:39 +02:00
Ruud fede348fbd Icon replacements 2013-05-10 15:14:47 +02:00
Ruud f3c60e8fa6 Added TPB proxies 2013-05-10 12:00:12 +02:00
Ruud 00e53439ed Don't wait between xbmc calls 2013-05-10 00:07:06 +02:00
Ruud 368fced0c4 Cancel autocomplete searches when starting new one 2013-05-10 00:03:42 +02:00
Ruud 666771fb0f Notification is empty styling 2013-05-10 00:02:11 +02:00
Ruud 9e3f978677 Styling fixes 2013-05-09 23:36:54 +02:00
Ruud f467d1c4f7 Dashboard thumbnails height not set properly. fix #1698 2013-05-08 12:54:34 +02:00
Ruud d8fc9d937e Filmweb userscript fix 2013-05-08 12:47:17 +02:00
596 changed files with 19848 additions and 79216 deletions
+20
View File
@@ -40,3 +40,23 @@ Linux (ubuntu / debian):
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato` * Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
* Add it to defaults. `sudo update-rc.d couchpotato defaults` * Add it to defaults. `sudo update-rc.d couchpotato defaults`
* Open your browser and go to: `http://localhost:5050/` * Open your browser and go to: `http://localhost:5050/`
FreeBSD :
* Update your ports tree `sudo portsnap fetch update`
* Install Python 2.6+ [lang/python](http://www.freshports.org/lang/python) with `cd /usr/ports/lang/python; sudo make install clean`
* Install port [databases/py-sqlite3](http://www.freshports.org/databases/py-sqlite3) with `cd /usr/ports/databases/py-sqlite3; sudo make install clean`
* Add a symlink to 'python2' `sudo ln -s /usr/local/bin/python /usr/local/bin/python2`
* Install port [ftp/libcurl](http://www.freshports.org/ftp/libcurl) with `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
* Install port [ftp/curl](http://www.freshports.org/ftp/bcurl), deselect 'Asynchronous DNS resolution via c-ares' when prompted as part of config `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
* Install port [textproc/docbook-xml-450](http://www.freshports.org/textproc/docbook-xml-450) with `cd /usr/ports/textproc/docbook-xml-450; sudo make install clean`
* Install port [GIT](http://git-scm.com/) with `cd /usr/ports/devel/git; sudo make install clean`
* 'cd' to the folder of your choosing.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato`
* Change the paths inside the init script. `sudo vim /etc/init.d/couchpotato`
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato`
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
* Open your browser and go to: `http://server:5050/`
+47 -46
View File
@@ -1,84 +1,85 @@
from couchpotato.api import api_docs, api_docs_missing from couchpotato.api import api_docs, api_docs_missing, api
from couchpotato.core.auth import requires_auth from couchpotato.core.auth import requires_auth
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import md5 from couchpotato.core.helpers.variable import md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.app import Flask
from flask.blueprints import Blueprint
from flask.globals import request
from flask.helpers import url_for
from flask.templating import render_template
from sqlalchemy.engine import create_engine from sqlalchemy.engine import create_engine
from sqlalchemy.orm import scoped_session from sqlalchemy.orm import scoped_session
from sqlalchemy.orm.session import sessionmaker from sqlalchemy.orm.session import sessionmaker
from werkzeug.utils import redirect from tornado import template
from tornado.web import RequestHandler
import os import os
import time import time
log = CPLog(__name__) log = CPLog(__name__)
app = Flask(__name__, static_folder = 'nope') views = {}
web = Blueprint('web', __name__) template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
# Main web handler
@requires_auth
class WebHandler(RequestHandler):
def get(self, route, *args, **kwargs):
route = route.strip('/')
if not views.get(route):
page_not_found(self)
return
self.write(views[route]())
def addView(route, func, static = False):
views[route] = func
def get_session(engine = None): def get_session(engine = None):
return Env.getSession(engine) return Env.getSession(engine)
def addView(route, func, static = False):
web.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func)
""" Web view """ # Web view
@web.route('/')
@requires_auth
def index(): def index():
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
addView('', index)
""" Api view """ # API docs
@web.route('docs/')
@requires_auth
def apiDocs(): def apiDocs():
from couchpotato import app
routes = [] routes = []
for route, x in sorted(app.view_functions.iteritems()):
if route[0:4] == 'api.': for route in api.iterkeys():
routes += [route[4:].replace('::', '.')] routes.append(route)
if api_docs.get(''): if api_docs.get(''):
del api_docs[''] del api_docs['']
del api_docs_missing[''] del api_docs_missing['']
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing))
@web.route('getkey/') return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env)
def getApiKey():
api = None addView('docs', apiDocs)
params = getParams()
username = Env.setting('username')
password = Env.setting('password')
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password): # Make non basic auth option to get api key
api = Env.setting('api_key') class KeyHandler(RequestHandler):
def get(self, *args, **kwargs):
api = None
username = Env.setting('username')
password = Env.setting('password')
return jsonified({ if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
'success': api is not None, api = Env.setting('api_key')
'api_key': api
})
@app.errorhandler(404) self.write({
def page_not_found(error): 'success': api is not None,
index_url = url_for('web.index') 'api_key': api
url = request.path[len(index_url):] })
def page_not_found(rh):
index_url = Env.get('web_base')
url = rh.request.uri[len(index_url):]
if url[:3] != 'api': if url[:3] != 'api':
if request.path != '/': r = index_url + '#' + url.lstrip('/')
r = request.url.replace(request.path, index_url + '#' + url) rh.redirect(r)
else:
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
return redirect(r)
else: else:
if not Env.get('dev'): if not Env.get('dev'):
time.sleep(0.1) time.sleep(0.1)
return 'Wrong API key used', 404
rh.set_status(404)
rh.write('Wrong API key used')
+48 -20
View File
@@ -1,20 +1,22 @@
from flask.blueprints import Blueprint from couchpotato.core.helpers.request import getParams
from flask.helpers import url_for
from tornado.web import RequestHandler, asynchronous from tornado.web import RequestHandler, asynchronous
from werkzeug.utils import redirect import json
import urllib
api = Blueprint('api', __name__) api = {}
api_docs = {}
api_docs_missing = []
api_nonblock = {} api_nonblock = {}
api_docs = {}
api_docs_missing = []
# NonBlock API handler
class NonBlockHandler(RequestHandler): class NonBlockHandler(RequestHandler):
stoppers = [] stoppers = []
@asynchronous @asynchronous
def get(self, route): def get(self, route, *args, **kwargs):
route = route.strip('/')
start, stop = api_nonblock[route] start, stop = api_nonblock[route]
self.stoppers.append(stop) self.stoppers.append(stop)
@@ -32,14 +34,6 @@ class NonBlockHandler(RequestHandler):
self.stoppers = [] self.stoppers = []
def addApiView(route, func, static = False, docs = None, **kwargs):
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
if docs:
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
else:
api_docs_missing.append(route)
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs): def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
api_nonblock[route] = func_tuple api_nonblock[route] = func_tuple
@@ -48,9 +42,43 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
else: else:
api_docs_missing.append(route) api_docs_missing.append(route)
""" Api view """ # Blocking API handler
def index(): class ApiHandler(RequestHandler):
index_url = url_for('web.index')
return redirect(index_url + 'docs/')
addApiView('', index) def get(self, route, *args, **kwargs):
route = route.strip('/')
if not api.get(route):
self.write('API call doesn\'t seem to exist')
return
kwargs = {}
for x in self.request.arguments:
kwargs[x] = urllib.unquote(self.get_argument(x))
# Split array arguments
kwargs = getParams(kwargs)
# Remove t random string
try: del kwargs['t']
except: pass
# Check JSONP callback
result = api[route](**kwargs)
jsonp_callback = self.get_argument('callback_func', default = None)
if jsonp_callback:
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
elif isinstance(result, (tuple)) and result[0] == 'redirect':
self.redirect(result[1])
else:
self.write(result)
def addApiView(route, func, static = False, docs = None, **kwargs):
if static: func(route)
else: api[route] = func
if docs:
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
else:
api_docs_missing.append(route)
+10 -11
View File
@@ -1,6 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import cleanHost, md5 from couchpotato.core.helpers.variable import cleanHost, md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -68,12 +67,12 @@ class Core(Plugin):
return True return True
def available(self): def available(self, **kwargs):
return jsonified({ return {
'success': True 'success': True
}) }
def shutdown(self): def shutdown(self, **kwargs):
if self.shutdown_started: if self.shutdown_started:
return False return False
@@ -83,7 +82,7 @@ class Core(Plugin):
return 'shutdown' return 'shutdown'
def restart(self): def restart(self, **kwargs):
if self.shutdown_started: if self.shutdown_started:
return False return False
@@ -156,10 +155,10 @@ class Core(Plugin):
host = 'localhost' host = 'localhost'
port = Env.setting('port') port = Env.setting('port')
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else '') return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), Env.get('web_base'))
def createApiUrl(self): def createApiUrl(self):
return '%s/api/%s' % (self.createBaseUrl(), Env.setting('api_key')) return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
def version(self): def version(self):
ver = fireEvent('updater.info', single = True) ver = fireEvent('updater.info', single = True)
@@ -170,10 +169,10 @@ class Core(Plugin):
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash']) return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
def versionView(self): def versionView(self, **kwargs):
return jsonified({ return {
'version': self.version() 'version': self.version()
}) }
def signalHandler(self): def signalHandler(self):
if Env.get('daemonized'): return if Env.get('daemonized'): return
+31 -4
View File
@@ -4,9 +4,10 @@ from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from minify.cssmin import cssmin
from minify.jsmin import jsmin from minify.jsmin import jsmin
import cssprefixer
import os import os
import re
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -109,7 +110,8 @@ class ClientScript(Plugin):
if file_type == 'script': if file_type == 'script':
data = jsmin(f) data = jsmin(f)
else: else:
data = cssprefixer.process(f, debug = False, minify = True) data = self.prefix(f)
data = cssmin(f)
data = data.replace('../images/', '../static/images/') data = data.replace('../images/', '../static/images/')
data = data.replace('../fonts/', '../static/fonts/') data = data.replace('../fonts/', '../static/fonts/')
data = data.replace('../../static/', '../static/') # Replace inside plugins data = data.replace('../../static/', '../static/') # Replace inside plugins
@@ -119,10 +121,10 @@ class ClientScript(Plugin):
# Combine all files together with some comments # Combine all files together with some comments
data = '' data = ''
for r in raw: for r in raw:
data += self.comment.get(file_type) % (r.get('file'), r.get('date')) data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
data += r.get('data') + '\n\n' data += r.get('data') + '\n\n'
self.createFile(out, ss(data.strip())) self.createFile(out, data.strip())
if not self.minified.get(file_type): if not self.minified.get(file_type):
self.minified[file_type] = {} self.minified[file_type] = {}
@@ -170,3 +172,28 @@ class ClientScript(Plugin):
if not self.paths[type].get(location): if not self.paths[type].get(location):
self.paths[type][location] = [] self.paths[type][location] = []
self.paths[type][location].append(file_path) self.paths[type][location].append(file_path)
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
prefix_tags = ['ms', 'moz', 'webkit']
def prefix(self, data):
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
new_data = ''
colon_split = trimmed_data.split(';')
for splt in colon_split:
curl_split = splt.strip().split('{')
for curly in curl_split:
curly = curly.strip()
for prop in self.prefix_properties:
if curly[:len(prop) + 1] == prop + ':':
for tag in self.prefix_tags:
new_data += ' -%s-%s; ' % (tag, curly)
new_data += curly + (' { ' if len(curl_split) > 1 else ' ')
new_data += '; '
new_data = new_data.replace('{ ;', '; ').replace('} ;', '} ')
return new_data
+17 -21
View File
@@ -1,7 +1,6 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -33,10 +32,10 @@ class Updater(Plugin):
else: else:
self.updater = SourceUpdater() self.updater = SourceUpdater()
addEvent('app.load', self.autoUpdate) addEvent('app.load', self.setCrons)
addEvent('updater.info', self.info) addEvent('updater.info', self.info)
addApiView('updater.info', self.getInfo, docs = { addApiView('updater.info', self.info, docs = {
'desc': 'Get updater information', 'desc': 'Get updater information',
'return': { 'return': {
'type': 'object', 'type': 'object',
@@ -62,7 +61,7 @@ class Updater(Plugin):
self.autoUpdate() # Check after enabling self.autoUpdate() # Check after enabling
def autoUpdate(self): def autoUpdate(self):
if self.check() and self.conf('automatic') and not self.updater.update_failed: if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
if self.updater.doUpdate(): if self.updater.doUpdate():
# Notify before restarting # Notify before restarting
@@ -80,31 +79,30 @@ class Updater(Plugin):
return False return False
def check(self): def check(self, force = False):
if self.isDisabled(): if not force and self.isDisabled():
return return
if self.updater.check(): if self.updater.check():
if not self.available_notified and self.conf('notification') and not self.conf('automatic'): if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info()) info = self.updater.info()
version_date = datetime.fromtimestamp(info['update_version']['date'])
fireEvent('updater.available', message = 'A new update with hash "%s" is available, this version is from %s' % (info['update_version']['hash'], version_date), data = info)
self.available_notified = True self.available_notified = True
return True return True
return False return False
def info(self): def info(self, **kwargs):
return self.updater.info() return self.updater.info()
def getInfo(self): def checkView(self, **kwargs):
return jsonified(self.updater.info()) return {
'update_available': self.check(force = True),
def checkView(self):
return jsonified({
'update_available': self.check(),
'info': self.updater.info() 'info': self.updater.info()
}) }
def doUpdateView(self): def doUpdateView(self, **kwargs):
self.check() self.check()
if not self.updater.update_version: if not self.updater.update_version:
@@ -119,9 +117,9 @@ class Updater(Plugin):
if not success: if not success:
success = True success = True
return jsonified({ return {
'success': success 'success': success
}) }
class BaseUpdater(Plugin): class BaseUpdater(Plugin):
@@ -138,9 +136,6 @@ class BaseUpdater(Plugin):
def doUpdate(self): def doUpdate(self):
pass pass
def getInfo(self):
return jsonified(self.info())
def info(self): def info(self):
return { return {
'last_check': self.last_check, 'last_check': self.last_check,
@@ -279,6 +274,7 @@ class SourceUpdater(BaseUpdater):
if download_data.get('type') == 'zip': if download_data.get('type') == 'zip':
zip = zipfile.ZipFile(destination) zip = zipfile.ZipFile(destination)
zip.extractall(extracted_path) zip.extractall(extracted_path)
zip.close()
else: else:
tar = tarfile.open(destination) tar = tarfile.open(destination)
tar.extractall(path = extracted_path) tar.extractall(path = extracted_path)
+30 -16
View File
@@ -1,26 +1,40 @@
from couchpotato.core.helpers.variable import md5 from couchpotato.core.helpers.variable import md5
from couchpotato.environment import Env from couchpotato.environment import Env
from flask import request, Response import base64
from functools import wraps
def check_auth(username, password): def check_auth(username, password):
return username == Env.setting('username') and password == Env.setting('password') return username == Env.setting('username') and password == Env.setting('password')
def authenticate(): def requires_auth(handler_class):
return Response(
'This is not the page you are looking for. *waves hand*', 401,
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'}
)
def requires_auth(f): def wrap_execute(handler_execute):
@wraps(f) def require_basic_auth(handler, kwargs):
def decorated(*args, **kwargs): if Env.setting('username') and Env.setting('password'):
auth = getattr(request, 'authorization')
if Env.setting('username') and Env.setting('password'):
if (not auth or not check_auth(auth.username.decode('latin1'), md5(auth.password.decode('latin1').encode(Env.get('encoding'))))):
return authenticate()
return f(*args, **kwargs) auth_header = handler.request.headers.get('Authorization')
auth_decoded = base64.decodestring(auth_header[6:]) if auth_header else None
if auth_decoded:
username, password = auth_decoded.split(':', 2)
return decorated if auth_header is None or not auth_header.startswith('Basic ') or (not check_auth(username.decode('latin'), md5(password.decode('latin')))):
handler.set_status(401)
handler.set_header('WWW-Authenticate', 'Basic realm="CouchPotato Login"')
handler._transforms = []
handler.finish()
return False
return True
def _execute(self, transforms, *args, **kwargs):
if not require_basic_auth(self, kwargs):
return False
return handler_execute(self, transforms, *args, **kwargs)
return _execute
handler_class._execute = wrap_execute(handler_class._execute)
return handler_class
+1 -1
View File
@@ -16,7 +16,7 @@ class Downloader(Provider):
torrent_sources = [ torrent_sources = [
'http://torrage.com/torrent/%s.torrent', 'http://torrage.com/torrent/%s.torrent',
'http://torcache.net/torrent/%s.torrent', 'https://torcache.net/torrent/%s.torrent',
] ]
torrent_trackers = [ torrent_trackers = [
+10 -1
View File
@@ -104,12 +104,21 @@ class NZBGet(Downloader):
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0] nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
except: except:
nzb_id = item['NZBID'] nzb_id = item['NZBID']
timeleft = -1
try:
if item['ActiveDownloads'] > 0 and item['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
timeleft = str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
except:
pass
statuses.append({ statuses.append({
'id': nzb_id, 'id': nzb_id,
'name': item['NZBFilename'], 'name': item['NZBFilename'],
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED', 'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1, 'timeleft': timeleft,
}) })
for item in queue: # 'Parameters' is not passed in rpc.postqueue for item in queue: # 'Parameters' is not passed in rpc.postqueue
@@ -11,7 +11,7 @@ config = [{
'list': 'download_providers', 'list': 'download_providers',
'name': 'sabnzbd', 'name': 'sabnzbd',
'label': 'Sabnzbd', 'label': 'Sabnzbd',
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> to download NZBs.', 'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
'wizard': True, 'wizard': True,
'options': [ 'options': [
{ {
@@ -18,7 +18,7 @@ config = [{
'name': 'enabled', 'name': 'enabled',
'default': 0, 'default': 0,
'type': 'enabler', 'type': 'enabler',
'radio_group': 'torrent', 'radio_group': 'nzb,torrent',
}, },
{ {
'name': 'host', 'name': 'host',
@@ -32,6 +32,13 @@ config = [{
'name': 'password', 'name': 'password',
'type': 'password', 'type': 'password',
}, },
{
'name': 'use_for',
'label': 'Use for',
'default': 'both',
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
},
{ {
'name': 'manual', 'name': 'manual',
'default': 0, 'default': 0,
+80 -43
View File
@@ -1,22 +1,21 @@
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import httplib
import json import json
import urllib import requests
import urllib2
log = CPLog(__name__) log = CPLog(__name__)
class Synology(Downloader): class Synology(Downloader):
type = ['torrent_magnet'] type = ['nzb', 'torrent', 'torrent_magnet']
log = CPLog(__name__) log = CPLog(__name__)
def download(self, data, movie, filedata = None): def download(self, data, movie, filedata = None):
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type'))) response = False
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['type']))
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = self.conf('host').split(':')
@@ -24,20 +23,41 @@ class Synology(Downloader):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
if data.get('type') == 'torrent':
log.error('Can\'t add binary torrent file')
return False
try: try:
# Send request to Transmission # Send request to Synology
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password')) srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
remote_torrent = srpc.add_torrent_uri(data.get('url')) if data['type'] == 'torrent_magnet':
log.info('Response: %s', remote_torrent) log.info('Adding torrent URL %s', data['url'])
return remote_torrent['success'] response = srpc.create_task(url = data['url'])
elif data['type'] in ['nzb', 'torrent']:
log.info('Adding %s' % data['type'])
if not filedata:
log.error('No %s data found' % data['type'])
else:
filename = data['name'] + '.' + data['type']
response = srpc.create_task(filename = filename, filedata = filedata)
except Exception, err: except Exception, err:
log.error('Exception while adding torrent: %s', err) log.error('Exception while adding torrent: %s', err)
return False finally:
return response
def getEnabledDownloadType(self):
if self.conf('use_for') == 'both':
return super(Synology, self).getEnabledDownloadType()
elif self.conf('use_for') == 'torrent':
return ['torrent', 'torrent_magnet']
else:
return ['nzb']
def isEnabled(self, manual, data = {}):
for_type = ['both']
if data and 'torrent' in data.get('type'):
for_type.append('torrent')
elif data:
for_type.append(data.get('type'))
return super(Synology, self).isEnabled(manual, data) and\
((self.conf('use_for') in for_type))
class SynologyRPC(object): class SynologyRPC(object):
@@ -58,11 +78,13 @@ class SynologyRPC(object):
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2, args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
'method': 'login', 'session': self.session_name, 'format': 'sid'} 'method': 'login', 'session': self.session_name, 'format': 'sid'}
response = self._req(self.auth_url, args) response = self._req(self.auth_url, args)
if response['success'] == True: if response['success']:
self.sid = response['data']['sid'] self.sid = response['data']['sid']
log.debug('Sid=%s', self.sid) log.debug('sid=%s', self.sid)
return response else:
elif self.username or self.password: log.error('Couldn\'t login to Synology, %s', response)
return response['success']
else:
log.error('User or password missing, not using authentication.') log.error('User or password missing, not using authentication.')
return False return False
@@ -70,36 +92,51 @@ class SynologyRPC(object):
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid} args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
return self._req(self.auth_url, args) return self._req(self.auth_url, args)
def _req(self, url, args): def _req(self, url, args, files = None):
req_url = url + '?' + urllib.urlencode(args) response = {'success': False}
try: try:
req_open = urllib2.urlopen(req_url) req = requests.post(url, data = args, files = files)
response = json.loads(req_open.read()) req.raise_for_status()
response = json.loads(req.text)
if response['success'] == True: if response['success'] == True:
log.info('Synology action successfull') log.info('Synology action successfull')
return response return response
except httplib.InvalidURL, err: except requests.ConnectionError, err:
log.error('Invalid Transmission host, check your config %s', err) log.error('Synology connection error, check your config %s', err)
return False except requests.HTTPError, err:
except urllib2.HTTPError, err:
log.error('SynologyRPC HTTPError: %s', err) log.error('SynologyRPC HTTPError: %s', err)
return False except Exception, err:
except urllib2.URLError, err: log.error('Exception: %s', err)
log.error('Unable to connect to Synology %s', err) finally:
return False return response
def add_torrent_uri(self, torrent): def create_task(self, url = None, filename = None, filedata = None):
log.info('Adding torrent URL %s', torrent) ''' Creates new download task in Synology DownloadStation. Either specify
response = {} url or pair (filename, filedata).
Returns True if task was created, False otherwise
'''
result = False
# login # login
login = self._login() if self._login():
if len(login) > 0 and login['success'] == True: args = {'api': 'SYNO.DownloadStation.Task',
log.info('Login success, adding torrent') 'version': '1',
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid} 'method': 'create',
response = self._req(self.download_url, args) '_sid': self.sid}
if url:
log.info('Login success, adding torrent URI')
args['uri'] = url
response = self._req(self.download_url, args = args)
log.info('Response: %s', response)
result = response['success']
elif filename and filedata:
log.info('Login success, adding torrent')
files = {'file': (filename, filedata)}
response = self._req(self.download_url, args = args, files = files)
log.info('Response: %s', response)
result = response['success']
else:
log.error('Invalid use of SynologyRPC.create_task: either url or filename+filedata must be specified')
self._logout() self._logout()
else:
log.error('Couldn\'t login to Synology, %s', login)
return response
return result
@@ -8,7 +8,6 @@ import httplib
import json import json
import os.path import os.path
import re import re
import shutil
import traceback import traceback
import urllib2 import urllib2
@@ -37,12 +36,7 @@ class Transmission(Downloader):
if len(self.conf('directory', default = '')) > 0: if len(self.conf('directory', default = '')) > 0:
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1] folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep) params['download-dir'] = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
# Create the empty folder to download too
self.makeDir(folder_path)
params['download-dir'] = folder_path
torrent_params = {} torrent_params = {}
if self.conf('ratio'): if self.conf('ratio'):
@@ -94,11 +88,13 @@ class Transmission(Downloader):
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio'] 'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio']
} }
queue = trpc.get_alltorrents(return_params) queue = trpc.get_alltorrents(return_params)
except Exception, err: except Exception, err:
log.error('Failed getting queue: %s', err) log.error('Failed getting queue: %s', err)
return False return False
if not queue:
return []
statuses = StatusList(self) statuses = StatusList(self)
# Get torrents status # Get torrents status
+21 -38
View File
@@ -3,10 +3,9 @@ from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader, StatusList from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt, ss from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta
from hashlib import sha1 from hashlib import sha1
from multipartpost import MultipartPostHandler from multipartpost import MultipartPostHandler
from datetime import timedelta
import os
import cookielib import cookielib
import httplib import httplib
import json import json
@@ -106,35 +105,6 @@ class uTorrent(Downloader):
return False return False
statuses = StatusList(self) statuses = StatusList(self)
download_folder = ''
settings_dict = {}
try:
data = self.utorrent_api.get_settings()
utorrent_settings = json.loads(data)
# Create settings dict
for item in utorrent_settings['settings']:
if item[1] == 0: # int
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
elif item[1] == 1: # bool
settings_dict[item[0]] = True if item[2] == 'true' else False
elif item[1] == 2: # string
settings_dict[item[0]] = item[2]
log.debug('uTorrent settings: %s', settings_dict)
# Get the download path from the uTorrent settings
if settings_dict['dir_completed_download_flag']:
download_folder = settings_dict['dir_completed_download']
elif settings_dict['dir_active_download_flag']:
download_folder = settings_dict['dir_active_download']
else:
log.info('No download folder set in uTorrent. Please set a download folder')
return False
except Exception, err:
log.error('Failed to get settings from uTorrent: %s', err)
return False
# Get torrents # Get torrents
for item in queue.get('torrents', []): for item in queue.get('torrents', []):
@@ -144,18 +114,13 @@ class uTorrent(Downloader):
if item[21] == 'Finished' or item[21] == 'Seeding': if item[21] == 'Finished' or item[21] == 'Seeding':
status = 'completed' status = 'completed'
if settings_dict['dir_add_label']:
release_folder = os.path.join(download_folder, item[11], item[2])
else:
release_folder = os.path.join(download_folder, item[2])
statuses.append({ statuses.append({
'id': item[0], 'id': item[0],
'name': item[2], 'name': item[2],
'status': status, 'status': status,
'original_status': item[1], 'original_status': item[1],
'timeleft': str(timedelta(seconds = item[10])), 'timeleft': str(timedelta(seconds = item[10])),
'folder': release_folder, 'folder': item[26],
}) })
return statuses return statuses
@@ -235,4 +200,22 @@ class uTorrentAPI(object):
def get_settings(self): def get_settings(self):
action = "action=getsettings" action = "action=getsettings"
return self._request(action) settings_dict = {}
try:
utorrent_settings = json.loads(self._request(action))
# Create settings dict
for item in utorrent_settings['settings']:
if item[1] == 0: # int
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
elif item[1] == 1: # bool
settings_dict[item[0]] = True if item[2] == 'true' else False
elif item[1] == 2: # string
settings_dict[item[0]] = item[2]
#log.debug('uTorrent settings: %s', settings_dict)
except Exception, err:
log.error('Failed to get settings from uTorrent: %s', err)
return settings_dict
+22 -11
View File
@@ -22,14 +22,22 @@ def addEvent(name, handler, priority = 100):
def createHandle(*args, **kwargs): def createHandle(*args, **kwargs):
try: try:
parent = handler.im_self # Open handler
bc = hasattr(parent, 'beforeCall') has_parent = hasattr(handler, 'im_self')
if bc: parent.beforeCall(handler) if has_parent:
parent = handler.im_self
bc = hasattr(parent, 'beforeCall')
if bc: parent.beforeCall(handler)
# Main event
h = runHandler(name, handler, *args, **kwargs) h = runHandler(name, handler, *args, **kwargs)
ac = hasattr(parent, 'afterCall')
if ac: parent.afterCall(handler) # Close handler
if has_parent:
ac = hasattr(parent, 'afterCall')
if ac: parent.afterCall(handler)
except: except:
h = runHandler(name, handler, *args, **kwargs) log.error('Failed creating handler %s %s: %s', (name, handler, traceback.format_exc()))
return h return h
@@ -43,7 +51,7 @@ def removeEvent(name, handler):
e -= handler e -= handler
def fireEvent(name, *args, **kwargs): def fireEvent(name, *args, **kwargs):
if not events.get(name): return if not events.has_key(name): return
e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock()) e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock())
@@ -133,14 +141,17 @@ def fireEvent(name, *args, **kwargs):
options['on_complete']() options['on_complete']()
return results return results
except KeyError, e:
pass
except Exception: except Exception:
log.error('%s: %s', (name, traceback.format_exc())) log.error('%s: %s', (name, traceback.format_exc()))
def fireEventAsync(*args, **kwargs): def fireEventAsync(*args, **kwargs):
kwargs['async'] = True try:
fireEvent(*args, **kwargs) t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
t.setDaemon(True)
t.start()
return True
except Exception, e:
log.error('%s: %s', (args[0], e))
def errorHandler(error): def errorHandler(error):
etype, value, tb = error etype, value, tb = error
+4 -32
View File
@@ -1,15 +1,11 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natcmp from couchpotato.core.helpers.variable import natcmp
from flask.globals import current_app
from flask.helpers import json, make_response
from urllib import unquote from urllib import unquote
from werkzeug.urls import url_decode
import flask
import re import re
def getParams():
params = url_decode(getattr(flask.request, 'environ').get('QUERY_STRING', '')) def getParams(params):
reg = re.compile('^[a-z0-9_\.]+$') reg = re.compile('^[a-z0-9_\.]+$')
current = temp = {} current = temp = {}
@@ -36,6 +32,8 @@ def getParams():
current = current[item] current = current[item]
else: else:
temp[param] = toUnicode(unquote(value)) temp[param] = toUnicode(unquote(value))
if temp[param].lower() in ['true', 'false']:
temp[param] = temp[param].lower() != 'false'
return dictToList(temp) return dictToList(temp)
@@ -54,29 +52,3 @@ def dictToList(params):
new = params new = params
return new return new
def getParam(attr, default = None):
try:
return getParams().get(attr, default)
except:
return default
def padded_jsonify(callback, *args, **kwargs):
content = str(callback) + '(' + json.dumps(dict(*args, **kwargs)) + ')'
return getattr(current_app, 'response_class')(content, mimetype = 'text/javascript')
def jsonify(mimetype, *args, **kwargs):
content = json.dumps(dict(*args, **kwargs))
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
def jsonified(*args, **kwargs):
callback = getParam('callback_func', None)
if callback:
content = padded_jsonify(callback, *args, **kwargs)
else:
content = jsonify('application/json', *args, **kwargs)
response = make_response(content)
response.cache_control.no_cache = True
return response
+3 -2
View File
@@ -181,5 +181,6 @@ def possibleTitles(raw_title):
def randomString(size = 8, chars = string.ascii_uppercase + string.digits): def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size)) return ''.join(random.choice(chars) for x in range(size))
def splitString(str, split_on = ','): def splitString(str, split_on = ',', clean = True):
return [x.strip() for x in str.split(split_on)] if str else [] list = [x.strip() for x in str.split(split_on)] if str else []
return filter(None, list) if clean else list
+3 -4
View File
@@ -1,11 +1,10 @@
import logging import logging
import re import re
import traceback
class CPLog(object): class CPLog(object):
context = '' context = ''
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key'] replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
def __init__(self, context = ''): def __init__(self, context = ''):
if context.endswith('.main'): if context.endswith('.main'):
@@ -50,8 +49,8 @@ class CPLog(object):
msg = msg % tuple([ss(x) for x in list(replace_tuple)]) msg = msg % tuple([ss(x) for x in list(replace_tuple)])
else: else:
msg = msg % ss(replace_tuple) msg = msg % ss(replace_tuple)
except: except Exception, e:
self.logger.error(u'Failed encoding stuff to log: %s' % traceback.format_exc()) self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e))
if not Env.get('dev'): if not Env.get('dev'):
+4 -3
View File
@@ -1,6 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -50,7 +49,7 @@ class Notification(Provider):
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
pass pass
def test(self): def test(self, **kwargs):
test_type = self.testNotifyName() test_type = self.testNotifyName()
@@ -62,7 +61,9 @@ class Notification(Provider):
listener = 'test' listener = 'test'
) )
return jsonified({'success': success}) return {
'success': success
}
def testNotifyName(self): def testNotifyName(self):
return 'notify.%s.test' % self.getName().lower() return 'notify.%s.test' % self.getName().lower()
+24 -17
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView, addNonBlockApiView from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
@@ -11,6 +10,7 @@ from couchpotato.environment import Env
from sqlalchemy.sql.expression import or_ from sqlalchemy.sql.expression import or_
import threading import threading
import time import time
import traceback
import uuid import uuid
log = CPLog(__name__) log = CPLog(__name__)
@@ -62,11 +62,9 @@ class CoreNotifier(Notification):
db.commit() db.commit()
def markAsRead(self): def markAsRead(self, ids = None, **kwargs):
ids = None ids = splitString(ids) if ids else None
if getParam('ids'):
ids = splitString(getParam('ids'))
db = get_session() db = get_session()
@@ -79,14 +77,13 @@ class CoreNotifier(Notification):
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def listView(self): def listView(self, limit_offset = None, **kwargs):
db = get_session() db = get_session()
limit_offset = getParam('limit_offset', None)
q = db.query(Notif) q = db.query(Notif)
@@ -105,11 +102,11 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
notifications.append(ndict) notifications.append(ndict)
return jsonified({ return {
'success': True, 'success': True,
'empty': len(notifications) == 0, 'empty': len(notifications) == 0,
'notifications': notifications 'notifications': notifications
}) }
def checkMessages(self): def checkMessages(self):
@@ -150,6 +147,8 @@ class CoreNotifier(Notification):
def frontend(self, type = 'notification', data = {}, message = None): def frontend(self, type = 'notification', data = {}, message = None):
log.debug('Notifying frontend')
self.m_lock.acquire() self.m_lock.acquire()
notification = { notification = {
'message_id': str(uuid.uuid4()), 'message_id': str(uuid.uuid4()),
@@ -168,11 +167,13 @@ class CoreNotifier(Notification):
'result': [notification], 'result': [notification],
}) })
except: except:
break log.debug('Failed sending to listener: %s', traceback.format_exc())
self.m_lock.release() self.m_lock.release()
self.cleanMessages() self.cleanMessages()
log.debug('Done notifying frontend')
def addListener(self, callback, last_id = None): def addListener(self, callback, last_id = None):
if last_id: if last_id:
@@ -194,9 +195,11 @@ class CoreNotifier(Notification):
if listener == callback: if listener == callback:
self.listeners.remove(list_tuple) self.listeners.remove(list_tuple)
except: except:
pass log.debug('Failed removing listener: %s', traceback.format_exc())
def cleanMessages(self): def cleanMessages(self):
log.debug('Cleaning messages')
self.m_lock.acquire() self.m_lock.acquire()
for message in self.messages: for message in self.messages:
@@ -204,8 +207,11 @@ class CoreNotifier(Notification):
self.messages.remove(message) self.messages.remove(message)
self.m_lock.release() self.m_lock.release()
log.debug('Done cleaning messages')
def getMessages(self, last_id): def getMessages(self, last_id):
log.debug('Getting messages with id: %s', last_id)
self.m_lock.acquire() self.m_lock.acquire()
recent = [] recent = []
@@ -216,15 +222,16 @@ class CoreNotifier(Notification):
recent = self.messages[index:] recent = self.messages[index:]
self.m_lock.release() self.m_lock.release()
log.debug('Returning for %s %s messages', (last_id, len(recent or [])))
return recent or [] return recent or []
def listener(self): def listener(self, init = False, **kwargs):
messages = [] messages = []
# Get unread # Get unread
if getParam('init'): if init:
db = get_session() db = get_session()
notifications = db.query(Notif) \ notifications = db.query(Notif) \
@@ -235,7 +242,7 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
messages.append(ndict) messages.append(ndict)
return jsonified({ return {
'success': True, 'success': True,
'result': messages, 'result': messages,
}) }
@@ -31,8 +31,8 @@ var NotificationBase = new Class({
}); });
window.addEvent('load', function(){ window.addEvent('load', function(){
self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 300, self) self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 100, self);
}); })
}, },
+10 -10
View File
@@ -1,7 +1,6 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import re import re
@@ -22,10 +21,7 @@ class NMJ(Notification):
addApiView(self.testNotifyName(), self.test) addApiView(self.testNotifyName(), self.test)
addApiView('notify.nmj.auto_config', self.autoConfig) addApiView('notify.nmj.auto_config', self.autoConfig)
def autoConfig(self): def autoConfig(self, host = 'localhost', **kwargs):
params = getParams()
host = params.get('host', 'localhost')
database = '' database = ''
mount = '' mount = ''
@@ -63,11 +59,11 @@ class NMJ(Notification):
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url') log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
return self.failed() return self.failed()
return jsonified({ return {
'success': True, 'success': True,
'database': database, 'database': database,
'mount': mount, 'mount': mount,
}) }
def addToLibrary(self, message = None, group = {}): def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
@@ -113,9 +109,13 @@ class NMJ(Notification):
return True return True
def failed(self): def failed(self):
return jsonified({'success': False}) return {
'success': False
}
def test(self): def test(self, **kwargs):
return jsonified({'success': self.addToLibrary()}) return {
'success': self.addToLibrary()
}
@@ -1,8 +1,8 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from flask.helpers import json
import base64 import base64
import json
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -22,6 +22,13 @@ config = [{
'description': 'Default should be on localhost', 'description': 'Default should be on localhost',
'advanced': True, 'advanced': True,
}, },
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
], ],
} }
], ],
+4 -3
View File
@@ -1,6 +1,5 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
@@ -73,7 +72,7 @@ class Plex(Notification):
log.info('Plex notification to %s successful.', host) log.info('Plex notification to %s successful.', host)
return True return True
def test(self): def test(self, **kwargs):
test_type = self.testNotifyName() test_type = self.testNotifyName()
@@ -86,4 +85,6 @@ class Plex(Notification):
) )
success2 = self.addToLibrary() success2 = self.addToLibrary()
return jsonified({'success': success or success2}) return {
'success': success or success2
}
@@ -23,7 +23,7 @@ class Pushover(Notification):
} }
if data and data.get('library'): if data and data.get('library'):
api_data.extend({ api_data.update({
'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']), 'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']),
'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])), 'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])),
}) })
@@ -1,5 +1,4 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import os import os
@@ -32,5 +31,7 @@ class Synoindex(Notification):
return True return True
def test(self): def test(self, **kwargs):
return jsonified({'success': os.path.isfile(self.index_path)}) return {
'success': os.path.isfile(self.index_path)
}
+11 -16
View File
@@ -1,12 +1,10 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from flask.helpers import url_for from couchpotato.environment import Env
from pytwitter import Api, parse_qsl from pytwitter import Api, parse_qsl
from werkzeug.utils import redirect
import oauth2 import oauth2
log = CPLog(__name__) log = CPLog(__name__)
@@ -70,10 +68,9 @@ class Twitter(Notification):
return True return True
def getAuthorizationUrl(self): def getAuthorizationUrl(self, host = None, **kwargs):
referer = getParam('host') callback_url = cleanHost(host) + '%snotify.%s.credentials/' % (Env.get('api_base').lstrip('/'), self.getName().lower())
callback_url = cleanHost(referer) + '%snotify.%s.credentials/' % (url_for('api.index').lstrip('/'), self.getName().lower())
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret) oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
oauth_client = oauth2.Client(oauth_consumer) oauth_client = oauth2.Client(oauth_consumer)
@@ -82,31 +79,29 @@ class Twitter(Notification):
if resp['status'] != '200': if resp['status'] != '200':
log.error('Invalid response from Twitter requesting temp token: %s', resp['status']) log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
return jsonified({ return {
'success': False, 'success': False,
}) }
else: else:
self.request_token = dict(parse_qsl(content)) self.request_token = dict(parse_qsl(content))
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token']) auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
log.info('Redirecting to "%s"', auth_url) log.info('Redirecting to "%s"', auth_url)
return jsonified({ return {
'success': True, 'success': True,
'url': auth_url, 'url': auth_url,
}) }
def getCredentials(self): def getCredentials(self, oauth_verifier, **kwargs):
key = getParam('oauth_verifier')
token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret']) token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret'])
token.set_verifier(key) token.set_verifier(oauth_verifier)
oauth_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret) oauth_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret)
oauth_client = oauth2.Client(oauth_consumer, token) oauth_client = oauth2.Client(oauth_consumer, token)
resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % key) resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % oauth_verifier)
access_token = dict(parse_qsl(content)) access_token = dict(parse_qsl(content))
if resp['status'] != '200': if resp['status'] != '200':
@@ -121,4 +116,4 @@ class Twitter(Notification):
self.request_token = None self.request_token = None
return redirect(url_for('web.index') + 'settings/notifications/') return 'redirect', Env.get('web_base') + 'settings/notifications/'
@@ -31,6 +31,20 @@ config = [{
'default': '', 'default': '',
'type': 'password', 'type': 'password',
}, },
{
'name': 'only_first',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Only update the first host when movie snatched, useful for synced XBMC',
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
], ],
} }
], ],
+37 -10
View File
@@ -1,8 +1,10 @@
from couchpotato.core.helpers.variable import splitString from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from flask.helpers import json from urllib2 import URLError
import base64 import base64
import json
import socket
import traceback import traceback
import urllib import urllib
@@ -13,25 +15,37 @@ class XBMC(Notification):
listen_to = ['renamer.after'] listen_to = ['renamer.after']
use_json_notifications = {} use_json_notifications = {}
http_time_between_calls = 0
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
hosts = splitString(self.conf('host')) hosts = splitString(self.conf('host'))
successful = 0 successful = 0
max_successful = 0
for host in hosts: for host in hosts:
if self.use_json_notifications.get(host) is None: if self.use_json_notifications.get(host) is None:
self.getXBMCJSONversion(host, message = message) self.getXBMCJSONversion(host, message = message)
if self.use_json_notifications.get(host): if self.use_json_notifications.get(host):
response = self.request(host, [ calls = [
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}), ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
('VideoLibrary.Scan', {}), ]
])
if not self.conf('only_first') or hosts.index(host) == 0:
calls.append(('VideoLibrary.Scan', {}))
max_successful += len(calls)
response = self.request(host, calls)
else: else:
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message}) response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
response += self.request(host, [('VideoLibrary.Scan', {})])
if not self.conf('only_first') or hosts.index(host) == 0:
response += self.request(host, [('VideoLibrary.Scan', {})])
max_successful += 1
max_successful += 1
try: try:
for result in response: for result in response:
@@ -43,7 +57,7 @@ class XBMC(Notification):
except: except:
log.error('Failed parsing results: %s', traceback.format_exc()) log.error('Failed parsing results: %s', traceback.format_exc())
return successful == len(hosts) * 2 return successful == max_successful
def getXBMCJSONversion(self, host, message = ''): def getXBMCJSONversion(self, host, message = ''):
@@ -52,7 +66,7 @@ class XBMC(Notification):
# XBMC JSON-RPC version request # XBMC JSON-RPC version request
response = self.request(host, [ response = self.request(host, [
('JSONRPC.Version', {}) ('JSONRPC.Version', {})
]) ])
for result in response: for result in response:
if (result.get('result') and type(result['result']['version']).__name__ == 'int'): if (result.get('result') and type(result['result']['version']).__name__ == 'int'):
# only v2 and v4 return an int object # only v2 and v4 return an int object
@@ -137,7 +151,7 @@ class XBMC(Notification):
# <li>Error:<message> # <li>Error:<message>
# </html> # </html>
# #
response = self.urlopen(server, headers = headers) response = self.urlopen(server, headers = headers, timeout = 3, show_error = False)
if 'OK' in response: if 'OK' in response:
log.debug('Returned from non-JSON-type request %s: %s', (host, response)) log.debug('Returned from non-JSON-type request %s: %s', (host, response))
@@ -148,6 +162,13 @@ class XBMC(Notification):
# manually fake expected response array # manually fake expected response array
return [{'result': 'Error'}] return [{'result': 'Error'}]
except URLError, e:
if isinstance(e.reason, socket.timeout):
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
return [{'result': 'Error'}]
else:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
return [{'result': 'Error'}]
except: except:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc()) log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
return [{'result': 'Error'}] return [{'result': 'Error'}]
@@ -176,11 +197,17 @@ class XBMC(Notification):
try: try:
log.debug('Sending request to %s: %s', (host, data)) log.debug('Sending request to %s: %s', (host, data))
rdata = self.urlopen(server, headers = headers, params = data, multipart = True) response = self.getJsonData(server, headers = headers, params = data, timeout = 3, show_error = False)
response = json.loads(rdata)
log.debug('Returned from request %s: %s', (host, response)) log.debug('Returned from request %s: %s', (host, response))
return response return response
except URLError, e:
if isinstance(e.reason, socket.timeout):
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
return []
else:
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return []
except: except:
log.error('Failed sending request to XBMC: %s', traceback.format_exc()) log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return [] return []
+39 -23
View File
@@ -1,12 +1,13 @@
from StringIO import StringIO from StringIO import StringIO
from couchpotato import addView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
toUnicode
from couchpotato.core.helpers.variable import getExt, md5 from couchpotato.core.helpers.variable import getExt, md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.templating import render_template_string
from multipartpost import MultipartPostHandler from multipartpost import MultipartPostHandler
from tornado import template
from tornado.web import StaticFileHandler
from urlparse import urlparse from urlparse import urlparse
import cookielib import cookielib
import glob import glob
@@ -28,6 +29,7 @@ class Plugin(object):
_needs_shutdown = False _needs_shutdown = False
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20130519 Firefox/24.0'
http_last_use = {} http_last_use = {}
http_time_between_calls = 0 http_time_between_calls = 0
http_failed_request = {} http_failed_request = {}
@@ -36,6 +38,7 @@ class Plugin(object):
def registerPlugin(self): def registerPlugin(self):
addEvent('app.do_shutdown', self.doShutdown) addEvent('app.do_shutdown', self.doShutdown)
addEvent('plugin.running', self.isRunning) addEvent('plugin.running', self.isRunning)
self._running = []
def conf(self, attr, value = None, default = None): def conf(self, attr, value = None, default = None):
return Env.setting(attr, self.getName().lower(), value = value, default = default) return Env.setting(attr, self.getName().lower(), value = value, default = default)
@@ -43,35 +46,37 @@ class Plugin(object):
def getName(self): def getName(self):
return self.__class__.__name__ return self.__class__.__name__
def renderTemplate(self, parent_file, template, **params): def renderTemplate(self, parent_file, templ, **params):
template = open(os.path.join(os.path.dirname(parent_file), template), 'r').read() t = template.Template(open(os.path.join(os.path.dirname(parent_file), templ), 'r').read())
return render_template_string(template, **params) return t.generate(**params)
def registerStatic(self, plugin_file, add_to_head = True): def registerStatic(self, plugin_file, add_to_head = True):
# Register plugin path # Register plugin path
self.plugin_path = os.path.dirname(plugin_file) self.plugin_path = os.path.dirname(plugin_file)
static_folder = toUnicode(os.path.join(self.plugin_path, 'static'))
if not os.path.isdir(static_folder):
return
# Get plugin_name from PluginName # Get plugin_name from PluginName
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__) s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__)
class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
# View path
path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name) path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name)
addView(path + '<path:filename>', self.showStatic, static = True)
# Add handler to Tornado
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + path + '(.*)', StaticFileHandler, {'path': static_folder})])
# Register for HTML <HEAD>
if add_to_head: if add_to_head:
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')): for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
ext = getExt(f) ext = getExt(f)
if ext in ['js', 'css']: if ext in ['js', 'css']:
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f) fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
def showStatic(self, filename):
d = os.path.join(self.plugin_path, 'static')
from flask.helpers import send_from_directory
return send_from_directory(d, filename)
def createFile(self, path, content, binary = False): def createFile(self, path, content, binary = False):
path = ss(path) path = ss(path)
@@ -104,12 +109,15 @@ class Plugin(object):
if not params: params = {} if not params: params = {}
# Fill in some headers # Fill in some headers
headers['Referer'] = headers.get('Referer', urlparse(url).hostname) parsed_url = urlparse(url)
headers['Host'] = headers.get('Host', urlparse(url).hostname) host = '%s%s' % (parsed_url.hostname, (':' + str(parsed_url.port) if parsed_url.port else ''))
headers['User-Agent'] = headers.get('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.2) Gecko/20100101 Firefox/10.0.2')
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
host = urlparse(url).hostname headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
headers['Host'] = headers.get('Host', host)
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
headers['Connection'] = headers.get('Connection', 'keep-alive')
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
# Don't try for failed requests # Don't try for failed requests
if self.http_failed_disabled.get(host, 0) > 0: if self.http_failed_disabled.get(host, 0) > 0:
@@ -126,6 +134,10 @@ class Plugin(object):
self.wait(host) self.wait(host)
try: try:
# Make sure opener has the correct headers
if opener:
opener.add_headers = headers
if multipart: if multipart:
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data')) log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
request = urllib2.Request(url, params, headers) request = urllib2.Request(url, params, headers)
@@ -138,8 +150,13 @@ class Plugin(object):
response = opener.open(request, timeout = timeout) response = opener.open(request, timeout = timeout)
else: else:
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()])) log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
data = tryUrlencode(params) if len(params) > 0 else None
if isinstance(params, (str, unicode)) and len(params) > 0:
data = params
else:
data = tryUrlencode(params) if len(params) > 0 else None
request = urllib2.Request(url, data, headers) request = urllib2.Request(url, data, headers)
if opener: if opener:
@@ -152,8 +169,10 @@ class Plugin(object):
buf = StringIO(response.read()) buf = StringIO(response.read())
f = gzip.GzipFile(fileobj = buf) f = gzip.GzipFile(fileobj = buf)
data = f.read() data = f.read()
f.close()
else: else:
data = response.read() data = response.read()
response.close()
self.http_failed_request[host] = 0 self.http_failed_request[host] = 0
except IOError: except IOError:
@@ -209,9 +228,6 @@ class Plugin(object):
def isRunning(self, value = None, boolean = True): def isRunning(self, value = None, boolean = True):
if not hasattr(self, '_running'):
self._running = []
if value is None: if value is None:
return self._running return self._running
+4 -6
View File
@@ -1,5 +1,4 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.helpers.variable import getUserDir from couchpotato.core.helpers.variable import getUserDir
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import ctypes import ctypes
@@ -63,16 +62,15 @@ class FileBrowser(Plugin):
return driveletters return driveletters
def view(self): def view(self, path = '/', show_hidden = True, **kwargs):
path = getParam('path', '/')
home = getUserDir() home = getUserDir()
if not path: if not path:
path = home path = home
try: try:
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True)) dirs = self.getDirectories(path = path, show_hidden = show_hidden)
except: except:
dirs = [] dirs = []
@@ -82,14 +80,14 @@ class FileBrowser(Plugin):
elif parent != '/' and parent[-2:] != ':\\': elif parent != '/' and parent[-2:] != ':\\':
parent += os.path.sep parent += os.path.sep
return jsonified({ return {
'is_root': path == '/', 'is_root': path == '/',
'empty': len(dirs) == 0, 'empty': len(dirs) == 0,
'parent': parent, 'parent': parent,
'home': home + os.path.sep, 'home': home + os.path.sep,
'platform': os.name, 'platform': os.name,
'dirs': dirs, 'dirs': dirs,
}) }
def is_hidden(self, filepath): def is_hidden(self, filepath):
+20 -44
View File
@@ -1,13 +1,12 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import splitString, tryInt from couchpotato.core.helpers.variable import splitString, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie from couchpotato.core.settings.model import Movie
from sqlalchemy.orm import joinedload_all from sqlalchemy.orm import joinedload_all
import random import random as rndm
import time import time
log = CPLog(__name__) log = CPLog(__name__)
@@ -16,41 +15,10 @@ log = CPLog(__name__)
class Dashboard(Plugin): class Dashboard(Plugin):
def __init__(self): def __init__(self):
addApiView('dashboard.suggestions', self.suggestView)
addApiView('dashboard.soon', self.getSoonView) addApiView('dashboard.soon', self.getSoonView)
def newSuggestions(self): def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs):
movies = fireEvent('movie.list', status = ['active', 'done'], limit_offset = (20, 0), single = True)
movie_identifiers = [m['library']['identifier'] for m in movies[1]]
ignored_movies = fireEvent('movie.list', status = ['ignored', 'deleted'], limit_offset = (100, 0), single = True)
ignored_identifiers = [m['library']['identifier'] for m in ignored_movies[1]]
suggestions = fireEvent('movie.suggest', movies = movie_identifiers, ignore = ignored_identifiers, single = True)
suggest_status = fireEvent('status.get', 'suggest', single = True)
for suggestion in suggestions:
fireEvent('movie.add', params = {'identifier': suggestion}, force_readd = False, search_after = False, status_id = suggest_status.get('id'))
def suggestView(self):
db = get_session()
movies = db.query(Movie).limit(20).all()
identifiers = [m.library.identifier for m in movies]
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
return jsonified({
'result': True,
'suggestions': suggestions
})
def getSoonView(self):
params = getParams()
db = get_session() db = get_session()
now = time.time() now = time.time()
@@ -73,7 +41,7 @@ class Dashboard(Plugin):
profile_pre[profile.get('id')] = contains profile_pre[profile.get('id')] = contains
# Get all active movies # Get all active movies
active_status = fireEvent('status.get', 'active', single = True) active_status, snatched_status, downloaded_status, available_status = fireEvent('status.get', ['active', 'snatched', 'downloaded', 'available'], single = True)
subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery() subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery()
q = db.query(Movie).join((subq, subq.c.id == Movie.id)) \ q = db.query(Movie).join((subq, subq.c.id == Movie.id)) \
@@ -85,7 +53,6 @@ class Dashboard(Plugin):
.options(joinedload_all('files')) .options(joinedload_all('files'))
# Add limit # Add limit
limit_offset = params.get('limit_offset')
limit = 12 limit = 12
if limit_offset: if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
@@ -93,8 +60,8 @@ class Dashboard(Plugin):
all_movies = q.all() all_movies = q.all()
if params.get('random', False): if random:
random.shuffle(all_movies) rndm.shuffle(all_movies)
movies = [] movies = []
for movie in all_movies: for movie in all_movies:
@@ -103,11 +70,19 @@ class Dashboard(Plugin):
coming_soon = False coming_soon = False
# Theater quality # Theater quality
if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, single = True): if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, movie.library.year, single = True):
coming_soon = True coming_soon = True
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True): if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, movie.library.year, single = True):
coming_soon = True coming_soon = True
# Skip if movie is snatched/downloaded/available
skip = False
for release in movie.releases:
if release.status_id in [snatched_status.get('id'), downloaded_status.get('id'), available_status.get('id')]:
skip = True
break
if skip:
continue
if coming_soon: if coming_soon:
temp = movie.to_dict({ temp = movie.to_dict({
@@ -118,17 +93,18 @@ class Dashboard(Plugin):
}) })
# Don't list older movies # Don't list older movies
if ((not params.get('late') and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \ if ((not late and ((not eta.get('dvd') and not eta.get('theater')) or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))): (late and (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200))):
movies.append(temp) movies.append(temp)
if len(movies) >= limit: if len(movies) >= limit:
break break
return jsonified({ db.expire_all()
return {
'success': True, 'success': True,
'empty': len(movies) == 0, 'empty': len(movies) == 0,
'movies': movies, 'movies': movies,
}) }
getLateView = getSoonView getLateView = getSoonView
+8 -16
View File
@@ -2,15 +2,13 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import md5, getExt from couchpotato.core.helpers.variable import md5, getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.scanner.main import Scanner from couchpotato.core.plugins.scanner.main import Scanner
from couchpotato.core.settings.model import FileType, File from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.helpers import send_file from tornado.web import StaticFileHandler
from werkzeug.exceptions import NotFound
import os.path import os.path
import time import time
import traceback import traceback
@@ -25,7 +23,7 @@ class FileManager(Plugin):
addEvent('file.download', self.download) addEvent('file.download', self.download)
addEvent('file.types', self.getTypes) addEvent('file.types', self.getTypes)
addApiView('file.cache/<path:filename>', self.showCacheFile, static = True, docs = { addApiView('file.cache/(.*)', self.showCacheFile, static = True, docs = {
'desc': 'Return a file from the cp_data/cache directory', 'desc': 'Return a file from the cp_data/cache directory',
'params': { 'params': {
'filename': {'desc': 'path/filename of the wanted file'} 'filename': {'desc': 'path/filename of the wanted file'}
@@ -73,7 +71,7 @@ class FileManager(Plugin):
db = get_session() db = get_session()
for root, dirs, walk_files in os.walk(Env.get('cache_dir')): for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
for filename in walk_files: for filename in walk_files:
if root == python_cache or 'minified' in filename or 'version' in filename: continue if root == python_cache or 'minified' in filename or 'version' in filename or 'temp_updater' in root: continue
file_path = os.path.join(root, filename) file_path = os.path.join(root, filename)
f = db.query(File).filter(File.path == toUnicode(file_path)).first() f = db.query(File).filter(File.path == toUnicode(file_path)).first()
if not f: if not f:
@@ -81,15 +79,9 @@ class FileManager(Plugin):
except: except:
log.error('Failed removing unused file: %s', traceback.format_exc()) log.error('Failed removing unused file: %s', traceback.format_exc())
def showCacheFile(self, filename = ''): def showCacheFile(self, route, **kwargs):
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': Env.get('cache_dir')})])
file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename))
if not os.path.isfile(file_path):
log.error('File "%s" not found', file_path)
raise NotFound()
return send_file(file_path, conditional = True)
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}): def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
@@ -158,8 +150,8 @@ class FileManager(Plugin):
return types return types
def getTypesView(self): def getTypesView(self, **kwargs):
return jsonified({ return {
'types': self.getTypes() 'types': self.getTypes()
}) }
+7 -4
View File
@@ -1,11 +1,11 @@
var File = new Class({ var File = new Class({
initialize: function(file){ initialize: function(type, file){
var self = this; var self = this;
if(!file){ if(!file){
self.empty = true; self.empty = true;
self.el = new Element('div'); self.el = new Element('div.empty_file.'+type);
return return
} }
@@ -22,7 +22,10 @@ var File = new Class({
var file_name = self.data.path.replace(/^.*[\\\/]/, ''); var file_name = self.data.path.replace(/^.*[\\\/]/, '');
self.el = new Element('div', { self.el = new Element('div', {
'class': 'type_image ' + self.type.identifier 'class': 'type_image ' + self.type.identifier,
'styles': {
'background-image': 'url('+Api.createUrl('file.cache') + file_name+')'
}
}).adopt( }).adopt(
new Element('img', { new Element('img', {
'src': Api.createUrl('file.cache') + file_name 'src': Api.createUrl('file.cache') + file_name
@@ -45,7 +48,7 @@ var FileSelect = new Class({
}); });
if(single) if(single)
return new File(results.pop()); return new File(type, results.pop());
return results; return results;
+7 -5
View File
@@ -1,7 +1,6 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, LibraryTitle, File from couchpotato.core.settings.model import Library, LibraryTitle, File
@@ -20,7 +19,6 @@ class LibraryPlugin(Plugin):
addEvent('library.update', self.update) addEvent('library.update', self.update)
addEvent('library.update_release_date', self.updateReleaseDate) addEvent('library.update_release_date', self.updateReleaseDate)
def add(self, attrs = {}, update_after = True): def add(self, attrs = {}, update_after = True):
db = get_session() db = get_session()
@@ -33,7 +31,8 @@ class LibraryPlugin(Plugin):
identifier = attrs.get('identifier'), identifier = attrs.get('identifier'),
plot = toUnicode(attrs.get('plot')), plot = toUnicode(attrs.get('plot')),
tagline = toUnicode(attrs.get('tagline')), tagline = toUnicode(attrs.get('tagline')),
status_id = status.get('id') status_id = status.get('id'),
info = {},
) )
title = LibraryTitle( title = LibraryTitle(
@@ -53,6 +52,7 @@ class LibraryPlugin(Plugin):
library_dict = l.to_dict(self.default_dict) library_dict = l.to_dict(self.default_dict)
db.expire_all()
return library_dict return library_dict
def update(self, identifier, default_title = '', force = False): def update(self, identifier, default_title = '', force = False):
@@ -87,7 +87,7 @@ class LibraryPlugin(Plugin):
library.tagline = toUnicode(info.get('tagline', '')) library.tagline = toUnicode(info.get('tagline', ''))
library.year = info.get('year', 0) library.year = info.get('year', 0)
library.status_id = done_status.get('id') library.status_id = done_status.get('id')
library.info = info library.info.update(info)
db.commit() db.commit()
# Titles # Titles
@@ -132,6 +132,7 @@ class LibraryPlugin(Plugin):
library_dict = library.to_dict(self.default_dict) library_dict = library.to_dict(self.default_dict)
db.expire_all()
return library_dict return library_dict
def updateReleaseDate(self, identifier): def updateReleaseDate(self, identifier):
@@ -147,9 +148,10 @@ class LibraryPlugin(Plugin):
if dates and dates.get('expires', 0) < time.time() or not dates: if dates and dates.get('expires', 0) < time.time() or not dates:
dates = fireEvent('movie.release_date', identifier = identifier, merge = True) dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
library.info = mergeDicts(library.info, {'release_date': dates }) library.info.update({'release_date': dates })
db.commit() db.commit()
db.expire_all()
return dates return dates
+18 -23
View File
@@ -1,6 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam, getParams
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -47,9 +46,9 @@ class Logging(Plugin):
} }
}) })
def get(self): def get(self, nr = 0, **kwargs):
nr = int(getParam('nr', 0)) nr = tryInt(nr)
current_path = None current_path = None
total = 1 total = 1
@@ -71,16 +70,15 @@ class Logging(Plugin):
f = open(current_path, 'r') f = open(current_path, 'r')
log = f.read() log = f.read()
return jsonified({ return {
'success': True, 'success': True,
'log': toUnicode(log), 'log': toUnicode(log),
'total': total, 'total': total,
}) }
def partial(self): def partial(self, type = 'all', lines = 30, **kwargs):
log_type = getParam('type', 'all') total_lines = tryInt(lines)
total_lines = tryInt(getParam('lines', 30))
log_lines = [] log_lines = []
@@ -100,7 +98,7 @@ class Logging(Plugin):
brk = False brk = False
for line in reversed_lines: for line in reversed_lines:
if log_type == 'all' or '%s ' % log_type.upper() in line: if type == 'all' or '%s ' % type.upper() in line:
log_lines.append(line) log_lines.append(line)
if len(log_lines) >= total_lines: if len(log_lines) >= total_lines:
@@ -111,12 +109,12 @@ class Logging(Plugin):
break break
log_lines.reverse() log_lines.reverse()
return jsonified({ return {
'success': True, 'success': True,
'log': '[0m\n'.join(log_lines), 'log': '[0m\n'.join(log_lines),
}) }
def clear(self): def clear(self, **kwargs):
for x in range(0, 50): for x in range(0, 50):
path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '') path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '')
@@ -135,24 +133,21 @@ class Logging(Plugin):
except: except:
log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc())) log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc()))
return jsonified({ return {
'success': True 'success': True
}) }
def log(self): def log(self, type = 'error', **kwargs):
params = getParams()
try: try:
log_message = 'API log: %s' % params log_message = 'API log: %s' % kwargs
try: try:
getattr(log, params.get('type', 'error'))(log_message) getattr(log, type)(log_message)
except: except:
log.error(log_message) log.error(log_message)
except: except:
log.error('Couldn\'t log via API: %s', params) log.error('Couldn\'t log via API: %s', kwargs)
return {
return jsonified({
'success': True 'success': True
}) }
@@ -6,19 +6,6 @@ Page.Log = new Class({
title: 'Show recent logs.', title: 'Show recent logs.',
has_tab: false, has_tab: false,
initialize: function(options){
var self = this;
self.parent(options)
App.getBlock('more').addLink(new Element('a', {
'href': App.createUrl(self.name),
'text': self.name.capitalize(),
'title': self.title
}))
},
indexAction: function(){ indexAction: function(){
var self = this; var self = this;
+9 -11
View File
@@ -1,7 +1,6 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import splitString, getTitle from couchpotato.core.helpers.variable import splitString, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -46,24 +45,23 @@ class Manage(Plugin):
}) })
if not Env.get('dev'): if not Env.get('dev'):
def updateLibrary(): addEvent('app.load', self.updateLibraryQuick)
self.updateLibrary(full = False)
addEvent('app.load', updateLibrary)
def getProgress(self): def getProgress(self, **kwargs):
return jsonified({ return {
'progress': self.in_progress 'progress': self.in_progress
}) }
def updateLibraryView(self): def updateLibraryView(self, full = 1, **kwargs):
full = getParam('full', default = 1)
fireEventAsync('manage.update', full = True if full == '1' else False) fireEventAsync('manage.update', full = True if full == '1' else False)
return jsonified({ return {
'success': True 'success': True
}) }
def updateLibraryQuick(self):
return self.updateLibrary(full = False)
def updateLibrary(self, full = True): def updateLibrary(self, full = True):
last_update = float(Env.prop('manage.last_update', default = 0)) last_update = float(Env.prop('manage.last_update', default = 0))
+59 -68
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.request import getParams, jsonified, getParam
from couchpotato.core.helpers.variable import getImdb, splitString from couchpotato.core.helpers.variable import getImdb, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -118,21 +117,21 @@ class MoviePlugin(Plugin):
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \ .filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
.all() .all()
#
for movie in movies: for movie in movies:
for rel in movie.releases: for rel in movie.releases:
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]: if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
fireEvent('release.delete', id = rel.id, single = True) fireEvent('release.delete', id = rel.id, single = True)
def getView(self): db.expire_all()
movie_id = getParam('id') def getView(self, id = None, **kwargs):
movie = self.get(movie_id) if movie_id else None
return jsonified({ movie = self.get(id) if id else None
return {
'success': movie is not None, 'success': movie is not None,
'movie': movie, 'movie': movie,
}) }
def get(self, movie_id): def get(self, movie_id):
@@ -149,6 +148,7 @@ class MoviePlugin(Plugin):
if m: if m:
results = m.to_dict(self.default_dict) results = m.to_dict(self.default_dict)
db.expire_all()
return results return results
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None): def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
@@ -174,8 +174,6 @@ class MoviePlugin(Plugin):
if release_status and len(release_status) > 0: if release_status and len(release_status) > 0:
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status])) q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
total_count = q.count()
filter_or = [] filter_or = []
if starts_with: if starts_with:
starts_with = toUnicode(starts_with.lower()) starts_with = toUnicode(starts_with.lower())
@@ -193,6 +191,8 @@ class MoviePlugin(Plugin):
if filter_or: if filter_or:
q = q.filter(or_(*filter_or)) q = q.filter(or_(*filter_or))
total_count = q.count()
if order == 'release_order': if order == 'release_order':
q = q.order_by(desc(Release.last_edit)) q = q.order_by(desc(Release.last_edit))
else: else:
@@ -216,15 +216,14 @@ class MoviePlugin(Plugin):
results = q2.all() results = q2.all()
movies = [] movies = []
for movie in results: for movie in results:
temp = movie.to_dict({ movies.append(movie.to_dict({
'profile': {'types': {}}, 'profile': {'types': {}},
'releases': {'files':{}, 'info': {}}, 'releases': {'files':{}, 'info': {}},
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
'files': {}, 'files': {},
}) }))
movies.append(temp)
#db.close() db.expire_all()
return (total_count, movies) return (total_count, movies)
def availableChars(self, status = None, release_status = None): def availableChars(self, status = None, release_status = None):
@@ -259,18 +258,17 @@ class MoviePlugin(Plugin):
if char not in chars: if char not in chars:
chars += str(char) chars += str(char)
#db.close() db.expire_all()
return ''.join(sorted(chars, key = str.lower)) return ''.join(sorted(chars, key = str.lower))
def listView(self): def listView(self, **kwargs):
params = getParams() status = splitString(kwargs.get('status', None))
status = splitString(params.get('status', None)) release_status = splitString(kwargs.get('release_status', None))
release_status = splitString(params.get('release_status', None)) limit_offset = kwargs.get('limit_offset', None)
limit_offset = params.get('limit_offset', None) starts_with = kwargs.get('starts_with', None)
starts_with = params.get('starts_with', None) search = kwargs.get('search', None)
search = params.get('search', None) order = kwargs.get('order', None)
order = params.get('order', None)
total_movies, movies = self.list( total_movies, movies = self.list(
status = status, status = status,
@@ -281,32 +279,31 @@ class MoviePlugin(Plugin):
order = order order = order
) )
return jsonified({ return {
'success': True, 'success': True,
'empty': len(movies) == 0, 'empty': len(movies) == 0,
'total': total_movies, 'total': total_movies,
'movies': movies, 'movies': movies,
}) }
def charView(self): def charView(self, **kwargs):
params = getParams() status = splitString(kwargs.get('status', None))
status = splitString(params.get('status', None)) release_status = splitString(kwargs.get('release_status', None))
release_status = splitString(params.get('release_status', None))
chars = self.availableChars(status, release_status) chars = self.availableChars(status, release_status)
return jsonified({ return {
'success': True, 'success': True,
'empty': len(chars) == 0, 'empty': len(chars) == 0,
'chars': chars, 'chars': chars,
}) }
def refresh(self): def refresh(self, id = '', **kwargs):
db = get_session() db = get_session()
for id in splitString(getParam('id')): for x in splitString(id):
movie = db.query(Movie).filter_by(id = id).first() movie = db.query(Movie).filter_by(id = x).first()
if movie: if movie:
@@ -315,18 +312,16 @@ class MoviePlugin(Plugin):
for title in movie.library.titles: for title in movie.library.titles:
if title.default: default_title = title.title if title.default: default_title = title.title
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True) fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id)) fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
db.expire_all()
#db.close() return {
return jsonified({
'success': True, 'success': True,
}) }
def search(self): def search(self, q = '', **kwargs):
q = getParam('q')
cache_key = u'%s/%s' % (__name__, simplifyString(q)) cache_key = u'%s/%s' % (__name__, simplifyString(q))
movies = Env.get('cache').get(cache_key) movies = Env.get('cache').get(cache_key)
@@ -338,11 +333,11 @@ class MoviePlugin(Plugin):
movies = fireEvent('movie.search', q = q, merge = True) movies = fireEvent('movie.search', q = q, merge = True)
Env.get('cache').set(cache_key, movies) Env.get('cache').set(cache_key, movies)
return jsonified({ return {
'success': True, 'success': True,
'empty': len(movies) == 0 if movies else 0, 'empty': len(movies) == 0 if movies else 0,
'movies': movies, 'movies': movies,
}) }
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None): def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
@@ -428,37 +423,34 @@ class MoviePlugin(Plugin):
if added: if added:
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')) fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
#db.close() db.expire_all()
return movie_dict return movie_dict
def addView(self): def addView(self, **kwargs):
params = getParams() movie_dict = self.add(params = kwargs)
movie_dict = self.add(params) return {
return jsonified({
'success': True, 'success': True,
'added': True if movie_dict else False, 'added': True if movie_dict else False,
'movie': movie_dict, 'movie': movie_dict,
}) }
def edit(self): def edit(self, id = '', **kwargs):
params = getParams()
db = get_session() db = get_session()
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
ids = splitString(params.get('id')) ids = splitString(id)
for movie_id in ids: for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first() m = db.query(Movie).filter_by(id = movie_id).first()
if not m: if not m:
continue continue
m.profile_id = params.get('profile_id') m.profile_id = kwargs.get('profile_id')
# Remove releases # Remove releases
for rel in m.releases: for rel in m.releases:
@@ -467,9 +459,9 @@ class MoviePlugin(Plugin):
db.commit() db.commit()
# Default title # Default title
if params.get('default_title'): if kwargs.get('default_title'):
for title in m.library.titles: for title in m.library.titles:
title.default = toUnicode(params.get('default_title', '')).lower() == toUnicode(title.title).lower() title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower()
db.commit() db.commit()
@@ -478,22 +470,20 @@ class MoviePlugin(Plugin):
movie_dict = m.to_dict(self.default_dict) movie_dict = m.to_dict(self.default_dict)
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id)) fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
#db.close() db.expire_all()
return jsonified({ return {
'success': True, 'success': True,
}) }
def deleteView(self): def deleteView(self, id = '', **kwargs):
params = getParams() ids = splitString(id)
ids = splitString(params.get('id'))
for movie_id in ids: for movie_id in ids:
self.delete(movie_id, delete_from = params.get('delete_from', 'all')) self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
return jsonified({ return {
'success': True, 'success': True,
}) }
def delete(self, movie_id, delete_from = None): def delete(self, movie_id, delete_from = None):
@@ -540,7 +530,7 @@ class MoviePlugin(Plugin):
if deleted: if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict()) fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
#db.close() db.expire_all()
return True return True
def restatus(self, movie_id): def restatus(self, movie_id):
@@ -568,7 +558,6 @@ class MoviePlugin(Plugin):
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id') m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
db.commit() db.commit()
#db.close()
return True return True
@@ -578,6 +567,7 @@ class MoviePlugin(Plugin):
db = get_session() db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first() movie = db.query(Movie).filter_by(id = movie_id).first()
fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id)) fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
db.expire_all()
return onComplete return onComplete
@@ -588,5 +578,6 @@ class MoviePlugin(Plugin):
db = get_session() db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first() movie = db.query(Movie).filter_by(id = movie_id).first()
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict)) fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
db.expire_all()
return notifyFront return notifyFront
+141 -82
View File
@@ -14,6 +14,7 @@ var MovieList = new Class({
movies: [], movies: [],
movies_added: {}, movies_added: {},
total_movies: 0,
letters: {}, letters: {},
filter: null, filter: null,
@@ -23,7 +24,7 @@ var MovieList = new Class({
self.offset = 0; self.offset = 0;
self.filter = self.options.filter || { self.filter = self.options.filter || {
'startswith': null, 'starts_with': null,
'search': null 'search': null
} }
@@ -48,7 +49,7 @@ var MovieList = new Class({
self.changeView('list'); self.changeView('list');
else else
self.changeView(self.getSavedView() || self.options.view || 'details'); self.changeView(self.getSavedView() || self.options.view || 'details');
self.getMovies(); self.getMovies();
App.addEvent('movie.added', self.movieAdded.bind(self)) App.addEvent('movie.added', self.movieAdded.bind(self))
@@ -62,7 +63,8 @@ var MovieList = new Class({
self.movies.each(function(movie){ self.movies.each(function(movie){
if(movie.get('id') == notification.data.id){ if(movie.get('id') == notification.data.id){
movie.destroy(); movie.destroy();
delete self.movies_added[notification.data.id] delete self.movies_added[notification.data.id];
self.setCounter(self.counter_count-1);
} }
}) })
} }
@@ -76,6 +78,7 @@ var MovieList = new Class({
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){ if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
window.scroll(0,0); window.scroll(0,0);
self.createMovie(notification.data, 'top'); self.createMovie(notification.data, 'top');
self.setCounter(self.counter_count+1);
self.checkIfEmpty(); self.checkIfEmpty();
} }
@@ -115,7 +118,7 @@ var MovieList = new Class({
self.createMovie(movie); self.createMovie(movie);
}); });
self.total_movies = total; self.total_movies += total;
self.setCounter(total); self.setCounter(total);
}, },
@@ -125,8 +128,41 @@ var MovieList = new Class({
if(!self.navigation_counter) return; if(!self.navigation_counter) return;
self.counter_count = count;
self.navigation_counter.set('text', (count || 0) + ' movies'); self.navigation_counter.set('text', (count || 0) + ' movies');
if (self.empty_message) {
self.empty_message.destroy();
self.empty_message = null;
}
if(self.total_movies && count == 0 && !self.empty_message){
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
self.empty_message = new Element('.message', {
'html': 'No movies found ' + message + '.<br/>'
}).grab(
new Element('a', {
'text': 'Reset filter',
'events': {
'click': function(){
self.filter = {
'starts_with': null,
'search': null
};
self.navigation_search_input.set('value', '');
self.reset();
self.activateLetter();
self.getMovies(true);
self.last_search_value = '';
}
}
})
).inject(self.movie_list);
}
}, },
createMovie: function(movie, inject_at){ createMovie: function(movie, inject_at){
@@ -151,66 +187,69 @@ var MovieList = new Class({
self.el.addClass('with_navigation') self.el.addClass('with_navigation')
self.navigation = new Element('div.alph_nav').grab( self.navigation = new Element('div.alph_nav').adopt(
new Element('div').adopt( self.mass_edit_form = new Element('div.mass_edit_form').adopt(
self.navigation_alpha = new Element('ul.numbers', { new Element('span.select').adopt(
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.massEditToggleAll.bind(self)
}
}),
self.mass_edit_selected = new Element('span.count', {'text': 0}),
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
),
new Element('div.quality').adopt(
self.mass_edit_quality = new Element('select'),
new Element('a.button.orange', {
'text': 'Change quality',
'events': {
'click': self.changeQualitySelected.bind(self)
}
})
),
new Element('div.delete').adopt(
new Element('span[text=or]'),
new Element('a.button.red', {
'text': 'Delete',
'events': {
'click': self.deleteSelected.bind(self)
}
})
),
new Element('div.refresh').adopt(
new Element('span[text=or]'),
new Element('a.button.green', {
'text': 'Refresh',
'events': {
'click': self.refreshSelected.bind(self)
}
})
)
),
new Element('div.menus').adopt(
self.navigation_counter = new Element('span.counter[title=Total]'),
self.filter_menu = new Block.Menu(self, {
'class': 'filter'
}),
self.navigation_actions = new Element('ul.actions', {
'events': { 'events': {
'click:relay(li)': function(e, el){ 'click:relay(li)': function(e, el){
self.movie_list.empty() var a = 'active';
self.activateLetter(el.get('data-letter')) self.navigation_actions.getElements('.'+a).removeClass(a);
self.getMovies() self.changeView(el.get('data-view'));
this.addClass(a);
el.inject(el.getParent(), 'top');
el.getSiblings().hide()
setTimeout(function(){
el.getSiblings().setStyle('display', null);
}, 100)
} }
} }
}), }),
self.navigation_counter = new Element('span.counter[title=Total]'), self.navigation_menu = new Block.Menu(self, {
self.navigation_actions = new Element('ul.inlay.actions.reversed'), 'class': 'extra'
self.navigation_search_input = new Element('input.search.inlay', { })
'title': 'Search through ' + self.options.identifier,
'placeholder': 'Search through ' + self.options.identifier,
'events': {
'keyup': self.search.bind(self),
'change': self.search.bind(self)
}
}),
self.navigation_menu = new Block.Menu(self),
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
new Element('span.select').adopt(
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.massEditToggleAll.bind(self)
}
}),
self.mass_edit_selected = new Element('span.count', {'text': 0}),
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
),
new Element('div.quality').adopt(
self.mass_edit_quality = new Element('select'),
new Element('a.button.orange', {
'text': 'Change quality',
'events': {
'click': self.changeQualitySelected.bind(self)
}
})
),
new Element('div.delete').adopt(
new Element('span[text=or]'),
new Element('a.button.red', {
'text': 'Delete',
'events': {
'click': self.deleteSelected.bind(self)
}
})
),
new Element('div.refresh').adopt(
new Element('span[text=or]'),
new Element('a.button.green', {
'text': 'Refresh',
'events': {
'click': self.refreshSelected.bind(self)
}
})
)
)
) )
).inject(self.el, 'top'); ).inject(self.el, 'top');
@@ -223,20 +262,39 @@ var MovieList = new Class({
}).inject(self.mass_edit_quality) }).inject(self.mass_edit_quality)
}); });
self.filter_menu.addLink(
self.navigation_search_input = new Element('input', {
'title': 'Search through ' + self.options.identifier,
'placeholder': 'Search through ' + self.options.identifier,
'events': {
'keyup': self.search.bind(self),
'change': self.search.bind(self)
}
})
).addClass('search');
self.filter_menu.addEvent('open', function(){
self.navigation_search_input.focus();
});
self.filter_menu.addLink(
self.navigation_alpha = new Element('ul.numbers', {
'events': {
'click:relay(li.available)': function(e, el){
self.activateLetter(el.get('data-letter'))
self.getMovies(true)
}
}
})
);
// Actions // Actions
['mass_edit', 'details', 'list'].each(function(view){ ['mass_edit', 'details', 'list'].each(function(view){
self.navigation_actions.adopt( var current = self.current_view == view;
new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', { new Element('li', {
'events': { 'class': 'icon2 ' + view + (current ? ' active ' : ''),
'click': function(e){ 'data-view': view
var a = 'active'; }).inject(self.navigation_actions, current ? 'top' : 'bottom');
self.navigation_actions.getElements('.'+a).removeClass(a);
self.changeView(this.get('data-view'));
this.addClass(a);
}
}
}).adopt(new Element('span'))
)
}); });
// All // All
@@ -260,11 +318,11 @@ var MovieList = new Class({
'status': self.options.status 'status': self.options.status
}, self.filter), }, self.filter),
'onSuccess': function(json){ 'onSuccess': function(json){
json.chars.split('').each(function(c){ json.chars.split('').each(function(c){
self.letters[c.capitalize()].addClass('available') self.letters[c.capitalize()].addClass('available')
}) })
} }
}); });
@@ -322,14 +380,14 @@ var MovieList = new Class({
self.movies.each(function(movie){ self.movies.each(function(movie){
if (movie.isSelected()){ if (movie.isSelected()){
$(movie).destroy() $(movie).destroy()
erase_movies.include(movie) erase_movies.include(movie);
} }
}); });
erase_movies.each(function(movie){ erase_movies.each(function(movie){
self.movies.erase(movie); self.movies.erase(movie);
movie.destroy();
movie.destroy() self.setCounter(self.counter_count-1);
}); });
self.calculateSelected(); self.calculateSelected();
@@ -448,8 +506,7 @@ var MovieList = new Class({
self.activateLetter(); self.activateLetter();
self.filter.search = search_value; self.filter.search = search_value;
self.movie_list.empty(); self.getMovies(true);
self.getMovies();
self.last_search_value = search_value; self.last_search_value = search_value;
@@ -461,11 +518,10 @@ var MovieList = new Class({
var self = this; var self = this;
self.reset(); self.reset();
self.movie_list.empty(); self.getMovies(true);
self.getMovies();
}, },
getMovies: function(){ getMovies: function(reset){
var self = this; var self = this;
if(self.scrollspy){ if(self.scrollspy){
@@ -492,10 +548,13 @@ var MovieList = new Class({
Api.request(self.options.api_call || 'movie.list', { Api.request(self.options.api_call || 'movie.list', {
'data': Object.merge({ 'data': Object.merge({
'status': self.options.status, 'status': self.options.status,
'limit_offset': self.options.limit + ',' + self.offset 'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
}, self.filter), }, self.filter),
'onSuccess': function(json){ 'onSuccess': function(json){
if(reset)
self.movie_list.empty();
if(self.loader_first){ if(self.loader_first){
var lf = self.loader_first; var lf = self.loader_first;
self.loader_first.addClass('hide') self.loader_first.addClass('hide')
@@ -1,9 +1,13 @@
var MovieAction = new Class({ var MovieAction = new Class({
Implements: [Options],
class_name: 'action icon', class_name: 'action icon2',
initialize: function(movie){ initialize: function(movie, options){
var self = this; var self = this;
self.setOptions(options);
self.movie = movie; self.movie = movie;
self.create(); self.create();
@@ -21,6 +25,32 @@ var MovieAction = new Class({
this.el.removeClass('disable') this.el.removeClass('disable')
}, },
getTitle: function(){
var self = this;
try {
return self.movie.getTitle();
}
catch(e){
try {
return self.movie.original_title ? self.movie.original_title : self.movie.titles[0];
}
catch(e){
return 'Unknown';
}
}
},
get: function(key){
var self = this;
try {
return self.movie.get(key)
}
catch(e){
return self.movie[key]
}
},
createMask: function(){ createMask: function(){
var self = this; var self = this;
self.mask = new Element('div.mask', { self.mask = new Element('div.mask', {
@@ -62,10 +92,10 @@ MA.IMDB = new Class({
create: function(){ create: function(){
var self = this; var self = this;
self.id = self.movie.get('identifier'); self.id = self.movie.get('imdb') || self.movie.get('identifier');
self.el = new Element('a.imdb', { self.el = new Element('a.imdb', {
'title': 'Go to the IMDB page of ' + self.movie.getTitle(), 'title': 'Go to the IMDB page of ' + self.getTitle(),
'href': 'http://www.imdb.com/title/'+self.id+'/', 'href': 'http://www.imdb.com/title/'+self.id+'/',
'target': '_blank' 'target': '_blank'
}); });
@@ -82,8 +112,8 @@ MA.Release = new Class({
create: function(){ create: function(){
var self = this; var self = this;
self.el = new Element('a.releases.icon.download', { self.el = new Element('a.releases.download', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(), 'title': 'Show the releases that are available for ' + self.getTitle(),
'events': { 'events': {
'click': self.show.bind(self) 'click': self.show.bind(self)
} }
@@ -100,10 +130,8 @@ MA.Release = new Class({
var self = this; var self = this;
if(!self.options_container){ if(!self.options_container){
self.options_container = new Element('div.options').adopt( self.options_container = new Element('div.options').grab(
self.release_container = new Element('div.releases.table').adopt( self.release_container = new Element('div.releases.table')
self.trynext_container = new Element('div.buttons.try_container')
)
); );
// Header // Header
@@ -138,7 +166,7 @@ MA.Release = new Class({
} }
// Create release // Create release
new Element('div', { var item = new Element('div', {
'class': 'item '+status.identifier, 'class': 'item '+status.identifier,
'id': 'release_'+release.id 'id': 'release_'+release.id
}).adopt( }).adopt(
@@ -149,11 +177,11 @@ MA.Release = new Class({
new Element('span.age', {'text': self.get(release, 'age')}), new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}), new Element('span.score', {'text': self.get(release, 'score')}),
new Element('span.provider', { 'text': provider, 'title': provider }), new Element('span.provider', { 'text': provider, 'title': provider }),
release.info['detail_url'] ? new Element('a.info.icon', { release.info['detail_url'] ? new Element('a.info.icon2', {
'href': release.info['detail_url'], 'href': release.info['detail_url'],
'target': '_blank' 'target': '_blank'
}) : null, }) : new Element('a'),
new Element('a.download.icon', { new Element('a.download.icon2', {
'events': { 'events': {
'click': function(e){ 'click': function(e){
(e).preventDefault(); (e).preventDefault();
@@ -162,16 +190,17 @@ MA.Release = new Class({
} }
} }
}), }),
new Element('a.delete.icon', { new Element('a.delete.icon2', {
'events': { 'events': {
'click': function(e){ 'click': function(e){
(e).preventDefault(); (e).preventDefault();
self.ignore(release); self.ignore(release);
this.getParent('.item').toggleClass('ignored')
} }
} }
}) })
).inject(self.release_container) ).inject(self.release_container);
release['el'] = item;
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){ if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched')) if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
@@ -190,7 +219,9 @@ MA.Release = new Class({
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release'); self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
} }
if(self.next_release || self.last_release){ if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
self.trynext_container.adopt( self.trynext_container.adopt(
new Element('span.or', { new Element('span.or', {
@@ -237,18 +268,19 @@ MA.Release = new Class({
(e).preventDefault(); (e).preventDefault();
self.createReleases(); self.createReleases();
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
if(self.next_release || self.last_release){ if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
self.trynext_container.adopt( self.trynext_container.adopt(
self.next_release ? [new Element('a.icon.readd', { self.next_release ? [new Element('a.icon2.readd', {
'text': self.last_release ? 'Download another release' : 'Download the best release', 'text': self.last_release ? 'Download another release' : 'Download the best release',
'events': { 'events': {
'click': self.tryNextRelease.bind(self) 'click': self.tryNextRelease.bind(self)
} }
}), }),
new Element('a.icon.download', { new Element('a.icon2.download', {
'text': 'pick one yourself', 'text': 'pick one yourself',
'events': { 'events': {
'click': function(){ 'click': function(){
@@ -256,7 +288,7 @@ MA.Release = new Class({
} }
} }
})] : null, })] : null,
new Element('a.icon.completed', { new Element('a.icon2.completed', {
'text': 'mark this movie done', 'text': 'mark this movie done',
'events': { 'events': {
'click': function(){ 'click': function(){
@@ -292,7 +324,7 @@ MA.Release = new Class({
var self = this; var self = this;
var release_el = self.release_container.getElement('#release_'+release.id), var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon'); icon = release_el.getElement('.download.icon2');
self.movie.busy(true); self.movie.busy(true);
@@ -317,6 +349,17 @@ MA.Release = new Class({
Api.request('release.ignore', { Api.request('release.ignore', {
'data': { 'data': {
'id': release.id 'id': release.id
},
'onComplete': function(){
var el = release.el;
if(el.hasClass('failed') || el.hasClass('ignored')){
el.removeClass('failed').removeClass('ignored');
el.getElement('.release_status').set('text', 'available');
}
else {
el.addClass('ignored');
el.getElement('.release_status').set('text', 'ignored');
}
} }
}) })
@@ -354,7 +397,7 @@ MA.Trailer = new Class({
var self = this; var self = this;
self.el = new Element('a.trailer', { self.el = new Element('a.trailer', {
'title': 'Watch the trailer of ' + self.movie.getTitle(), 'title': 'Watch the trailer of ' + self.getTitle(),
'events': { 'events': {
'click': self.watch.bind(self) 'click': self.watch.bind(self)
} }
@@ -367,12 +410,12 @@ MA.Trailer = new Class({
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18' var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
var url = data_url.substitute({ var url = data_url.substitute({
'title': encodeURI(self.movie.getTitle()), 'title': encodeURI(self.getTitle()),
'year': self.movie.get('year'), 'year': self.get('year'),
'offset': offset || 1 'offset': offset || 1
}), }),
size = $(self.movie).getSize(), size = $(self.movie).getSize(),
height = (size.x/16)*9, height = self.options.height || (size.x/16)*9,
id = 'trailer-'+randomString(); id = 'trailer-'+randomString();
self.player_container = new Element('div[id='+id+']'); self.player_container = new Element('div[id='+id+']');
+310 -207
View File
@@ -8,12 +8,23 @@
.movies > div { .movies > div {
clear: both; clear: both;
} }
.movies > div .message {
display: block;
padding: 20px;
font-size: 20px;
color: white;
text-align: center;
}
.movies > div .message a {
padding: 20px;
display: block;
}
.movies.thumbs_list > div:not(.description) { .movies.thumbs_list > div:not(.description) {
margin-right: -4px; margin-right: -4px;
text-align: center;
} }
.movies .loading { .movies .loading {
display: block; display: block;
padding: 20px 0 0 0; padding: 20px 0 0 0;
@@ -32,26 +43,26 @@
margin-top: -20px; margin-top: -20px;
overflow: hidden; overflow: hidden;
} }
.movies .loading .spinner { .movies .loading .spinner {
display: inline-block; display: inline-block;
} }
.movies .loading .message { .movies .loading .message {
margin: 0 20px; margin: 0 20px;
} }
.movies h2 { .movies h2 {
margin-bottom: 20px; margin-bottom: 20px;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies h2 { .movies h2 {
font-size: 25px; font-size: 25px;
margin-bottom: 10px; margin-bottom: 10px;
} }
} }
.movies > .description { .movies > .description {
position: absolute; position: absolute;
top: 30px; top: 30px;
@@ -62,41 +73,53 @@
.movies:hover > .description { .movies:hover > .description {
opacity: 1; opacity: 1;
} }
@media all and (max-width: 860px) { @media all and (max-width: 860px) {
.movies > .description { .movies > .description {
display: none; display: none;
} }
} }
.movies.thumbs_list { .movies.thumbs_list {
padding: 20px 0 20px; padding: 20px 0 20px;
} }
.home .movies { .home .movies {
padding-top: 6px; padding-top: 6px;
} }
.movies .movie { .movies .movie {
position: relative; position: relative;
border-radius: 4px;
margin: 10px 0; margin: 10px 0;
padding-left: 20px; padding-left: 20px;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
height: 180px; height: 180px;
transition: all 0.6s cubic-bezier(0.9,0,0.1,1); transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
transition-property: width, height;
background: rgba(0,0,0,.2);
} }
.movies.mass_edit_list .movie {
padding-left: 22px;
background: none;
}
.movies.details_list .movie { .movies.details_list .movie {
padding-left: 120px; padding-left: 120px;
} }
.movies.list_list .movie:not(.details_view), .movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie { .movies.mass_edit_list .movie {
height: 32px; height: 30px;
border-bottom: 1px solid rgba(255,255,255,.15);
} }
.movies.list_list .movie:last-child,
.movies.mass_edit_list .movie:last-child {
border: none;
}
.movies.thumbs_list .movie { .movies.thumbs_list .movie {
width: 16.66667%; width: 16.66667%;
height: auto; height: auto;
@@ -104,11 +127,8 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
vertical-align: top; vertical-align: top;
border-radius: 0;
box-shadow: none;
border: 0;
} }
@media all and (max-width: 800px) { @media all and (max-width: 800px) {
.movies.thumbs_list .movie { .movies.thumbs_list .movie {
width: 25%; width: 25%;
@@ -125,16 +145,7 @@
.movies.list_list .movie:not(.details_view), .movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie { .movies.mass_edit_list .movie {
margin: 1px 0; margin: 0;
border-radius: 0;
background: no-repeat;
box-shadow: none;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.movies.list_list .movie:hover:not(.details_view),
.movies.mass_edit_list .movie {
background: rgba(255,255,255,0.03);
} }
.movies .data { .movies .data {
@@ -142,19 +153,19 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
position: relative; position: relative;
right: 0;
border-radius: 0;
transition: all .6s cubic-bezier(0.9,0,0.1,1); transition: all .6s cubic-bezier(0.9,0,0.1,1);
right: 0;
} }
.movies.list_list .movie:not(.details_view) .data, .movies.list_list .movie:not(.details_view) .data,
.movies.mass_edit_list .movie .data { .movies.mass_edit_list .movie .data {
height: 30px; padding: 0 0 0 10px;
padding: 3px 0 3px 10px;
box-shadow: none;
border: 0; border: 0;
background: #4e5969; background: #4e5969;
} }
.movies.mass_edit_list .movie .data {
padding-left: 8px;
}
.movies.thumbs_list .data { .movies.thumbs_list .data {
position: absolute; position: absolute;
left: 0; left: 0;
@@ -165,15 +176,7 @@
background: none; background: none;
transition: none; transition: none;
} }
.movies.thumbs_list .movie.no_thumbnail .data { background-image: linear-gradient(-30deg, rgba(255, 0, 85, .2) 0,rgba(125, 185, 235, .2) 100%);
}
.movies.thumbs_list .movie.no_thumbnail:nth-child(2n+6) .data { background-image: linear-gradient(-20deg, rgba(125, 0, 215, .2) 0, rgba(4, 55, 5, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(3n+6) .data { background-image: linear-gradient(-30deg, rgba(155, 0, 85, .2) 0,rgba(25, 185, 235, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(4n+6) .data { background-image: linear-gradient(-30deg, rgba(115, 5, 235, .2) 0, rgba(55, 180, 5, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(5n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 215, 115, .7) 100%); }
.movies.thumbs_list .movie.no_thumbnail:nth-child(6n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 15, 115, .7) 100%); }
.movies.thumbs_list .movie:hover .data { .movies.thumbs_list .movie:hover .data {
background: rgba(0,0,0,0.9); background: rgba(0,0,0,0.9);
} }
@@ -201,28 +204,32 @@
line-height: 0; line-height: 0;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
border-radius: 4px 0 0 4px;
transition: all .6s cubic-bezier(0.9,0,0.1,1); transition: all .6s cubic-bezier(0.9,0,0.1,1);
background: rgba(0,0,0,.1);
} }
.movies.thumbs_list .poster { .movies.thumbs_list .poster {
position: relative; position: relative;
border-radius: 0;
} }
.movies.list_list .movie:not(.details_view) .poster, .movies.list_list .movie:not(.details_view) .poster,
.movies.mass_edit_list .poster { .movies.mass_edit_list .poster {
width: 20px; width: 20px;
height: 30px; height: 30px;
border-radius: 1px 0 0 1px;
} }
.movies.mass_edit_list .poster { .movies.mass_edit_list .poster {
display: none; display: none;
} }
.movies.thumbs_list .poster { .movies.thumbs_list .poster {
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: none; transition: none;
background: no-repeat center;
background-size: cover;
} }
.movies.thumbs_list .no_thumbnail .empty_file {
width: 100%;
height: 100%;
}
.movies .poster img, .movies .poster img,
.options .poster img { .options .poster img {
@@ -234,9 +241,9 @@
width: 100%; width: 100%;
top: 0; top: 0;
bottom: 0; bottom: 0;
border-radius: 0; opacity: 0;
} }
.movies .info { .movies .info {
position: relative; position: relative;
height: 100%; height: 100%;
@@ -248,31 +255,41 @@
font-weight: bold; font-weight: bold;
margin-bottom: 10px; margin-bottom: 10px;
margin-top: 2px; margin-top: 2px;
left: 0;
top: 0;
width: 100%; width: 100%;
padding-right: 80px; padding-right: 80px;
transition: all 0.2s linear; transition: all 0.2s linear;
height: 35px;
top: -5px;
position: relative;
} }
.movies.list_list .info .title,
.movies.mass_edit_list .info .title {
height: 100%;
top: 0;
margin: 0;
}
.touch_enabled .movies.list_list .info .title {
display: inline-block;
padding-right: 55px;
}
.movies .info .title span { .movies .info .title span {
display: block; display: inline-block;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
width: 100%; width: 100%;
height: 30px; height: 100%;
line-height: 30px; line-height: 30px;
top: -5px;
position: relative;
} }
.movies.thumbs_list .info .title span { .movies.thumbs_list .info .title span {
white-space: normal; white-space: normal;
overflow: auto; overflow: auto;
height: auto; height: auto;
text-align: left; text-align: left;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies.thumbs_list .movie .info .title span, .movies.thumbs_list .movie .info .title span,
.movies.thumbs_list .movie .info .year { .movies.thumbs_list .movie .info .year {
@@ -281,32 +298,33 @@
overflow: hidden; overflow: hidden;
} }
} }
.movies.list_list .movie:not(.details_view) .info .title, .movies.list_list .movie:not(.details_view) .info .title,
.movies.mass_edit_list .info .title { .movies.mass_edit_list .info .title {
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
width: auto; width: auto;
} }
.movies.thumbs_list .movie:not(.no_thumbnail) .info { .movies.thumbs_list .movie:not(.no_thumbnail) .info {
display: none; display: none;
} }
.movies.thumbs_list .movie:hover .info { .movies.thumbs_list .movie:hover .info {
display: block; display: block;
} }
.movies.thumbs_list .info .title { .movies.thumbs_list .info .title {
font-size: 21px; font-size: 21px;
word-wrap: break-word; word-wrap: break-word;
padding: 0; padding: 0;
height: 100%;
} }
.movies .info .year { .movies .info .year {
position: absolute; position: absolute;
color: #bbb; color: #bbb;
right: 0; right: 0;
top: 1px; top: 6px;
text-align: right; text-align: right;
transition: all 0.2s linear; transition: all 0.2s linear;
font-weight: normal; font-weight: normal;
@@ -316,7 +334,7 @@
font-size: 1.25em; font-size: 1.25em;
right: 10px; right: 10px;
} }
.movies.thumbs_list .info .year { .movies.thumbs_list .info .year {
font-size: 23px; font-size: 23px;
margin: 0; margin: 0;
@@ -327,6 +345,10 @@
color: #FFF; color: #FFF;
} }
.touch_enabled .movies.list_list .movie .info .year {
font-size: 1em;
}
.movies .info .description { .movies .info .description {
top: 30px; top: 30px;
clear: both; clear: both;
@@ -349,13 +371,24 @@
display: block; display: block;
min-height: 20px; min-height: 20px;
} }
.movies.list_list .movie:hover .data .quality {
display: none;
}
.touch_enabled .movies.list_list .movie .data .quality {
position: relative;
display: inline-block;
margin: 0;
top: -4px;
}
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies .data .quality { .movies .data .quality {
display: none; display: none;
} }
} }
.movies .status_suggest .data .quality, .movies .status_suggest .data .quality,
.movies.thumbs_list .data .quality { .movies.thumbs_list .data .quality {
display: none; display: none;
@@ -382,15 +415,15 @@
right: 0; right: 0;
margin-right: 60px; margin-right: 60px;
z-index: 1; z-index: 1;
top: 5px;
} }
.movies .data .quality .available, .movies .data .quality .available,
.movies .data .quality .snatched { .movies .data .quality .snatched {
opacity: 1; opacity: 1;
box-shadow: 1px 1px 0 rgba(0,0,0,0.2);
cursor: pointer; cursor: pointer;
} }
.movies .data .quality .available { background-color: #578bc3; } .movies .data .quality .available { background-color: #578bc3; }
.movies .data .quality .snatched { background-color: #369545; } .movies .data .quality .snatched { background-color: #369545; }
.movies .data .quality .done { .movies .data .quality .done {
@@ -407,59 +440,71 @@
.movies .data .actions { .movies .data .actions {
position: absolute; position: absolute;
bottom: 20px; bottom: 17px;
right: 20px; right: 20px;
line-height: 0; line-height: 0;
top: 0; top: 0;
display: block;
width: auto;
opacity: 0; opacity: 0;
display: none; display: none;
width: 0;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies .data .actions { .movies .data .actions {
display: none !important; display: none !important;
} }
} }
.movies .movie:hover .data .actions { .movies .movie:hover .data .actions,
.touch_enabled .movies .movie .data .actions {
opacity: 1; opacity: 1;
display: inline-block; display: inline-block;
width: auto;
} }
.movies.details_list .data .actions { .movies.details_list .data .actions {
top: auto; top: auto;
bottom: 18px; bottom: 18px;
} }
.movies .movie:hover .actions { .movies .movie:hover .actions {
opacity: 1; opacity: 1;
display: inline-block; display: inline-block;
} }
.movies.thumbs_list .data .actions { .movies.thumbs_list .data .actions {
bottom: 2px; bottom: 12px;
right: 10px; right: 10px;
top: auto; top: auto;
} }
.movies .movie:hover .action { opacity: 0.6; } .movies .movie:hover .action { opacity: 0.6; }
.movies .movie:hover .action:hover { opacity: 1; } .movies .movie:hover .action:hover { opacity: 1; }
.movies .data .action { .movies .data .action {
background-repeat: no-repeat;
background-position: center;
display: inline-block; display: inline-block;
width: 26px; height: 22px;
height: 26px; min-width: 33px;
padding: 3px; padding: 0 5px;
line-height: 26px;
text-align: center;
font-size: 13px;
color: #FFF;
margin-left: 1px;
} }
.movies .data .action.trailer { color: #FFF; }
.movies .data .action.download { color: #b9dec0; }
.movies .data .action.edit { color: #c6b589; }
.movies .data .action.refresh { color: #cbeecc; }
.movies .data .action.delete { color: #e9b0b0; }
.movies .data .action.directory { color: #ffed92; }
.movies .data .action.readd { color: #c2fac5; }
.movies.mass_edit_list .movie .data .actions { .movies.mass_edit_list .movie .data .actions {
display: none; display: none;
} }
.movies.list_list .movie:not(.details_view):hover .actions, .movies.list_list .movie:not(.details_view):hover .actions,
.movies.mass_edit_list .movie:hover .actions { .movies.mass_edit_list .movie:hover .actions,
.touch_enabled .movies.list_list .movie:not(.details_view) .actions {
margin: 0; margin: 0;
background: #4e5969; background: #4e5969;
top: 2px; top: 2px;
@@ -473,12 +518,10 @@
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
position: absolute; position: absolute;
padding: 70px 0 0; padding: 80px 0 0;
left: 120px; left: 120px;
right: 0; right: 0;
} }
.movies .delete_container .cancel {
}
.movies .delete_container .or { .movies .delete_container .or {
padding: 10px; padding: 10px;
} }
@@ -498,9 +541,9 @@
} }
.movies .options .form { .movies .options .form {
margin: 70px 20px 0; margin: 80px 0 0;
float: left;
font-size: 20px; font-size: 20px;
text-align: center;
} }
.movies .options .form select { .movies .options .form select {
@@ -510,16 +553,21 @@
.movies .options .table { .movies .options .table {
height: 180px; height: 180px;
overflow: auto; overflow: auto;
line-height: 2em;
} }
.movies .options .table .item { .movies .options .table .item {
border-bottom: 1px solid rgba(255,255,255,0.1); border-bottom: 1px solid rgba(255,255,255,0.1);
} }
.movies .options .table .item.ignored span { .movies .options .table .item.ignored span,
.movies .options .table .item.failed span {
text-decoration: line-through; text-decoration: line-through;
color: rgba(255,255,255,0.4); color: rgba(255,255,255,0.4);
} }
.movies .options .table .item.ignored .delete { .movies .options .table .item.ignored .delete:before,
background-image: url('../images/icon.undo.png'); .movies .options .table .item.failed .delete:before {
display: inline-block;
content: "\e04b";
transform: scale(-1, 1);
} }
.movies .options .table .item:last-child { border: 0; } .movies .options .table .item:last-child { border: 0; }
@@ -565,10 +613,13 @@
width: 30px !important; width: 30px !important;
height: 20px; height: 20px;
opacity: 0.8; opacity: 0.8;
line-height: 25px;
} }
.movies .options .table a:hover { .movies .options .table a:hover { opacity: 1; }
opacity: 1; .movies .options .table a.download { color: #a7fbaf; }
} .movies .options .table a.delete { color: #fda3a3; }
.movies .options .table .ignored a.delete,
.movies .options .table .failed a.delete { color: #b5fda3; }
.movies .options .table .head > * { .movies .options .table .head > * {
font-weight: bold; font-weight: bold;
@@ -578,7 +629,7 @@
height: auto; height: auto;
} }
.movies .movie .trailer_container { .trailer_container {
width: 100%; width: 100%;
background: #000; background: #000;
text-align: center; text-align: center;
@@ -588,11 +639,11 @@
position: absolute; position: absolute;
z-index: 10; z-index: 10;
} }
.movies .movie .trailer_container.hide { .trailer_container.hide {
height: 0 !important; height: 0 !important;
} }
.movies .movie .hide_trailer { .hide_trailer {
position: absolute; position: absolute;
top: 0; top: 0;
left: 50%; left: 50%;
@@ -601,11 +652,10 @@
text-align: center; text-align: center;
padding: 3px 10px; padding: 3px 10px;
background: #4e5969; background: #4e5969;
border-radius: 0 0 2px 2px;
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s; transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
z-index: 11; z-index: 11;
} }
.movies .movie .hide_trailer.hide { .hide_trailer.hide {
top: -30px; top: -30px;
} }
@@ -637,17 +687,18 @@
.movies .movie .trynext { .movies .movie .trynext {
display: inline; display: inline;
position: absolute; position: absolute;
right: 135px; right: 180px;
z-index: 2; z-index: 2;
opacity: 0; opacity: 0;
background: #4e5969; background: #4e5969;
min-width: 300px;
text-align: right; text-align: right;
height: 100%; height: 100%;
padding: 3px 0;
top: 0; top: 0;
} }
.touch_enabled .movies .movie .trynext {
display: none;
}
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies .movie .trynext { .movies .movie .trynext {
display: none; display: none;
@@ -655,31 +706,42 @@
} }
.movies.mass_edit_list .trynext { display: none; } .movies.mass_edit_list .trynext { display: none; }
.wanted .movies .movie .trynext { .wanted .movies .movie .trynext {
padding-right: 50px; padding-right: 30px;
} }
.movies .movie:hover .trynext { .movies .movie:hover .trynext,
.touch_enabled .movies.details_list .movie .trynext {
opacity: 1; opacity: 1;
} }
.movies.details_list .movie .trynext { .movies.details_list .movie .trynext {
background: #47515f; background: #47515f;
padding: 0; padding: 0;
right: 0; right: 0;
bottom: 35px; height: 25px;
height: auto;
} }
.movies .movie .trynext a { .movies .movie .trynext a {
background-position: 5px center; background-position: 5px center;
padding: 0 5px 0 25px; padding: 0 5px 0 25px;
margin-right: 10px; margin-right: 10px;
color: #FFF; color: #FFF;
border-radius: 2px; height: 100%;
line-height: 27px;
font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
} }
.movies .movie .trynext a:last-child { .movies .movie .trynext a:before {
margin: 0; margin: 2px 0 0 -20px;
position: absolute;
font-family: 'Elusive-Icons';
} }
.movies .movie .trynext a:hover { .movies.details_list .movie .trynext a {
line-height: 23px;
}
.movies .movie .trynext a:last-child {
margin: 0;
}
.movies .movie .trynext a:hover,
.touch_enabled .movies .movie .trynext a {
background-color: #369545; background-color: #369545;
} }
@@ -694,14 +756,7 @@
} }
.movies .alph_nav { .movies .alph_nav {
transition: box-shadow .4s linear; height: 44px;
position: relative;
z-index: 4;
top: 0px;
right: 0;
margin: 0 auto;
width: 100%;
padding: 10px 0;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
@@ -710,12 +765,9 @@
} }
} }
.movies .alph_nav > div { .movies .alph_nav .menus {
position: relative; display: inline-block;
max-width: 980px; float: right;
margin: 0 auto;
padding: 0;
min-height: 24px;
} }
.movies .alph_nav .numbers, .movies .alph_nav .numbers,
@@ -724,118 +776,152 @@
list-style: none; list-style: none;
padding: 0 0 1px; padding: 0 0 1px;
margin: 0; margin: 0;
float: left;
user-select: none; user-select: none;
} }
.movies .alph_nav .counter { .movies .alph_nav .counter {
display: inline-block;
text-align: right; text-align: right;
position: absolute; padding: 0 10px;
right: 270px; height: 100%;
background: #4e5969; line-height: 43px;
padding: 4px 10px; border-right: 1px solid rgba(255,255,255,.07);
} }
.movies .alph_nav .numbers li, .movies .alph_nav .numbers li,
.movies .alph_nav .actions li { .movies .alph_nav .actions li {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
height: 24px; height: 100%;
line-height: 23px; line-height: 30px;
text-align: center; text-align: center;
cursor: pointer;
color: rgba(255,255,255,0.2);
border: 1px solid transparent; border: 1px solid transparent;
transition: all 0.1s ease-in-out; transition: all 0.1s ease-in-out;
} }
@media all and (max-width: 900px) {
.movies .alph_nav .numbers {
display: none;
}
}
.movies .alph_nav .numbers li { .movies .alph_nav .numbers li {
width: auto; width: 30px;
padding: 0 4px; height: 30px;
opacity: 0.3;
} }
.movies .alph_nav .numbers li.letter_all {
width: 60px;
}
.movies .alph_nav li.available { .movies .alph_nav li.available {
color: #FFF; font-weight: bold;
font-weight: bolder; cursor: pointer;
opacity: 1;
} }
.movies .alph_nav li.active.available, .movies .alph_nav li.active.available,
.movies .alph_nav li.available:hover { .movies .alph_nav li.available:hover {
background: rgba(255,255,255,.1); background: rgba(0,0,0,.1);
} }
.movies .alph_nav .search { .movies .alph_nav .search input {
padding: 6px 5px; padding: 6px 5px;
margin: 0 0 0 20px; width: 100%;
position: absolute; height: 44px;
right: 30px; display: inline-block;
width: 154px; border: 0;
height: 25px; background: none;
transition: all 0.6s cubic-bezier(0.9,0,0.1,1); color: #444;
font-size: 14px;
padding: 10px;
padding: 0 10px 0 30px;
border-bottom: 1px solid rgba(0,0,0,.08);
} }
.movies .alph_nav .search input:focus {
background: rgba(0,0,0,.08);
}
.movies .alph_nav .search input::-webkit-input-placeholder {
color: #444;
opacity: .6;
}
.movies .alph_nav .search:before {
font-family: 'Elusive-Icons';
content: "\e03e";
position: absolute;
height: 20px;
line-height: 45px;
font-size: 12px;
margin: 0 0 0 10px;
opacity: .6;
color: #444;
}
.movies .alph_nav .actions { .movies .alph_nav .actions {
margin: 0 6px 0 0;
-moz-user-select: none; -moz-user-select: none;
position: absolute; width: 44px;
right: 183px; height: 44px;
display: inline-block;
vertical-align: top;
z-index: 200;
position: relative;
border: 1px solid rgba(255,255,255,.07);
border-width: 0 1px;
} }
.movies .alph_nav .actions:hover {
box-shadow: 0 100px 20px -10px rgba(0,0,0,0.55);
}
.movies .alph_nav .actions li { .movies .alph_nav .actions li {
border-radius: 1px; width: 100%;
width: auto; height: 45px;
line-height: 40px;
position: relative;
z-index: 20;
display: none;
cursor: pointer;
} }
.movies .alph_nav .actions li.active { .movies .alph_nav .actions:hover li:not(.active) {
background: none; display: block;
border: 1px solid transparent; background: #FFF;
box-shadow: none; color: #444;
} }
.movies .alph_nav .actions li span { .movies .alph_nav .actions li:hover:not(.active) {
display: block; background: #ccc;
background: url('../images/sprite.png') no-repeat; }
width: 25px; .movies .alph_nav .actions li.active {
height: 100%; display: block;
}
.movies .alph_nav .actions li.mass_edit span {
background-position: 3px 3px;
} }
.movies .alph_nav .actions li.list span { .movies .alph_nav .actions li.mass_edit:before {
background-position: 3px -95px; content: "\e070";
} }
.movies .alph_nav .actions li.details span { .movies .alph_nav .actions li.list:before {
background-position: 3px -74px; content: "\e0d8";
} }
.movies .alph_nav .actions li:first-child { .movies .alph_nav .actions li.details:before {
border-radius: 3px 0 0 3px; content: "\e022";
} }
.movies .alph_nav .actions li:last-child {
border-radius: 0 3px 3px 0;
}
.movies .alph_nav .mass_edit_form { .movies .alph_nav .mass_edit_form {
clear: both; clear: both;
text-align: center; text-align: center;
display: none; display: none;
overflow: hidden;
float: left;
height: 44px;
line-height: 44px;
} }
.movies.mass_edit_list .mass_edit_form { .movies.mass_edit_list .mass_edit_form {
display: block; display: inline-block;
} }
.movies.mass_edit_list .mass_edit_form .select { .movies.mass_edit_list .mass_edit_form .select {
float: left;
margin: 5px 0 0 5px;
font-size: 14px; font-size: 14px;
display: inline-block;
} }
.movies.mass_edit_list .mass_edit_form .select span { .movies.mass_edit_list .mass_edit_form .select .check {
display: inline-block;
vertical-align: middle; vertical-align: middle;
margin: -4px 0 0 5px;
}
.movies.mass_edit_list .mass_edit_form .select span {
opacity: 0.7; opacity: 0.7;
} }
.movies.mass_edit_list .mass_edit_form .select .count { .movies.mass_edit_list .mass_edit_form .select .count {
@@ -844,8 +930,7 @@
} }
.movies .alph_nav .mass_edit_form .quality { .movies .alph_nav .mass_edit_form .quality {
float: left; display: inline-block;
padding: 8px 0 0;
margin: 0 0 0 16px; margin: 0 0 0 16px;
} }
.movies .alph_nav .mass_edit_form .quality select { .movies .alph_nav .mass_edit_form .quality select {
@@ -858,8 +943,8 @@
.movies .alph_nav .mass_edit_form .refresh, .movies .alph_nav .mass_edit_form .refresh,
.movies .alph_nav .mass_edit_form .delete { .movies .alph_nav .mass_edit_form .delete {
float: left; display: inline-block;
padding: 8px 0 0 8px; margin-left: 8px;
} }
.movies .alph_nav .mass_edit_form .refresh span, .movies .alph_nav .mass_edit_form .refresh span,
@@ -867,28 +952,48 @@
margin: 0 10px 0 0; margin: 0 10px 0 0;
} }
.movies .alph_nav .more_menu {
right: 0;
position: absolute;
}
.movies .alph_nav .more_menu > a { .movies .alph_nav .more_menu > a {
background-color: #4e5969; background: none;
background-position: center -158px;
} }
.movies .alph_nav .more_menu.extra > a:before {
content: '...';
font-size: 1.7em;
line-height: 23px;
text-align: center;
display: block;
}
.movies .alph_nav .more_menu.filter {
}
.movies .alph_nav .more_menu.filter > a:before {
content: "\e0e8";
font-family: 'Elusive-Icons';
line-height: 33px;
display: block;
text-align: center;
}
.movies .alph_nav .more_menu.filter .wrapper {
right: 88px;
width: 300px;
}
.movies .empty_wanted { .movies .empty_wanted {
background-image: url('../images/emptylist.png'); background-image: url('../images/emptylist.png');
background-position: 80% 0;
height: 750px; height: 750px;
width: 800px; width: 100%;
max-width: 900px;
padding-top: 260px; padding-top: 260px;
margin-top: -50px;
} }
.movies .empty_manage { .movies .empty_manage {
text-align: center; text-align: center;
font-size: 25px; font-size: 25px;
line-height: 150%; line-height: 150%;
padding: 40px 0;
} }
.movies .empty_manage .after_manage { .movies .empty_manage .after_manage {
@@ -897,7 +1002,6 @@
} }
.movies .progress { .movies .progress {
border-radius: 2px;
padding: 10px; padding: 10px;
margin: 5px 0; margin: 5px 0;
text-align: left; text-align: left;
@@ -912,7 +1016,6 @@
width: 49%; width: 49%;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
margin: 2px 0.5%; margin: 2px 0.5%;
border-radius: 3px;
} }
.movies .progress > div .folder { .movies .progress > div .folder {
+8 -12
View File
@@ -11,7 +11,7 @@ var Movie = new Class({
self.view = options.view || 'details'; self.view = options.view || 'details';
self.list = list; self.list = list;
self.el = new Element('div.movie.inlay'); self.el = new Element('div.movie');
self.profile = Quality.getProfile(data.profile_id) || {}; self.profile = Quality.getProfile(data.profile_id) || {};
self.parent(self, options); self.parent(self, options);
@@ -23,7 +23,8 @@ var Movie = new Class({
var self = this; var self = this;
App.addEvent('movie.update.'+self.data.id, function(notification){ App.addEvent('movie.update.'+self.data.id, function(notification){
self.busy(false) self.busy(false);
self.removeView();
self.update.delay(2000, self, notification); self.update.delay(2000, self, notification);
}); });
@@ -107,6 +108,7 @@ var Movie = new Class({
self.data = notification.data; self.data = notification.data;
self.el.empty(); self.el.empty();
self.removeView();
self.profile = Quality.getProfile(self.data.profile_id) || {}; self.profile = Quality.getProfile(self.data.profile_id) || {};
self.create(); self.create();
@@ -139,9 +141,6 @@ var Movie = new Class({
'text': self.data.library.year || 'n/a' 'text': self.data.library.year || 'n/a'
}) })
), ),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', { self.description = new Element('div.description', {
'text': self.data.library.plot 'text': self.data.library.plot
}), }),
@@ -149,8 +148,8 @@ var Movie = new Class({
'events': { 'events': {
'click': function(e){ 'click': function(e){
var releases = self.el.getElement('.actions .releases'); var releases = self.el.getElement('.actions .releases');
if(releases) if(releases.isVisible())
releases.fireEvent('click', [e]) releases.fireEvent('click', [e])
} }
} }
}) })
@@ -199,9 +198,6 @@ var Movie = new Class({
self.actions.adopt(action) self.actions.adopt(action)
}); });
if(!self.data.library.rating)
self.rating.hide();
}, },
addQuality: function(quality_id){ addQuality: function(quality_id){
@@ -244,10 +240,10 @@ var Movie = new Class({
if(direction == 'in'){ if(direction == 'in'){
self.temp_view = self.view; self.temp_view = self.view;
self.changeView('details') self.changeView('details');
self.el.addEvent('outerClick', function(){ self.el.addEvent('outerClick', function(){
self.removeView() self.removeView();
self.slide('out') self.slide('out')
}) })
el.show(); el.show();
@@ -44,9 +44,8 @@
.search_form .input input { .search_form .input input {
border-radius: 0; border-radius: 0;
display: block; display: block;
width: 100%;
border: 0; border: 0;
background: rgba(255,255,255,.08); background: none;
color: #FFF; color: #FFF;
font-size: 25px; font-size: 25px;
height: 100%; height: 100%;
@@ -106,7 +105,7 @@
background: #5c697b; background: #5c697b;
margin: 4px 0 0; margin: 4px 0 0;
width: 470px; width: 470px;
min-height: 140px; min-height: 50px;
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55); box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
display: none; display: none;
} }
@@ -194,7 +193,7 @@
transition: all .4s cubic-bezier(0.9,0,0.1,1); transition: all .4s cubic-bezier(0.9,0,0.1,1);
} }
.movie_result .data.open { .movie_result .data.open {
left: 100%; left: 100% !important;
} }
.movie_result:last-child .data { border-bottom: 0; } .movie_result:last-child .data { border-bottom: 0; }
+32 -20
View File
@@ -98,6 +98,9 @@ Block.Search = new Class({
self.el[self.q() ? 'addClass' : 'removeClass']('filled') self.el[self.q() ? 'addClass' : 'removeClass']('filled')
if(self.q() != self.last_q){ if(self.q() != self.last_q){
if(self.api_request && self.api_request.isRunning())
self.api_request.cancel();
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer) if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
self.autocomplete_timer = self.autocomplete.delay(300, self) self.autocomplete_timer = self.autocomplete.delay(300, self)
} }
@@ -116,12 +119,9 @@ Block.Search = new Class({
}, },
list: function(){ list: function(){
var self = this; var self = this,
q = self.q(),
if(self.api_request && self.api_request.running) return cache = self.cache[q];
var q = self.q();
var cache = self.cache[q];
self.hideResults(false); self.hideResults(false);
@@ -164,9 +164,6 @@ Block.Search = new Class({
}); });
if(q != self.q())
self.list()
// Calculate result heights // Calculate result heights
var w = window.getSize(), var w = window.getSize(),
rc = self.result_container.getCoordinates(); rc = self.result_container.getCoordinates();
@@ -188,8 +185,11 @@ Block.Search = new Class({
Block.Search.Item = new Class({ Block.Search.Item = new Class({
Implements: [Options, Events],
initialize: function(info, options){ initialize: function(info, options){
var self = this; var self = this;
self.setOptions(options);
self.info = info; self.info = info;
self.alternative_titles = []; self.alternative_titles = [];
@@ -211,17 +211,13 @@ Block.Search.Item = new Class({
}) : null, }) : null,
self.options_el = new Element('div.options.inlay'), self.options_el = new Element('div.options.inlay'),
self.data_container = new Element('div.data', { self.data_container = new Element('div.data', {
'tween': {
duration: 400,
transition: 'quint:in:out'
},
'events': { 'events': {
'click': self.showOptions.bind(self) 'click': self.showOptions.bind(self)
} }
}).adopt( }).adopt(
new Element('div.info').adopt( new Element('div.info').adopt(
self.title = new Element('h2', { self.title = new Element('h2', {
'text': info.titles[0] 'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
}).adopt( }).adopt(
self.year = info.year ? new Element('span.year', { self.year = info.year ? new Element('span.year', {
'text': info.year 'text': info.year
@@ -231,12 +227,12 @@ Block.Search.Item = new Class({
) )
) )
if(info.titles)
info.titles.each(function(title){ info.titles.each(function(title){
self.alternativeTitle({ self.alternativeTitle({
'title': title 'title': title
}); });
}) })
}, },
alternativeTitle: function(alternative){ alternativeTitle: function(alternative){
@@ -245,6 +241,20 @@ Block.Search.Item = new Class({
self.alternative_titles.include(alternative); self.alternative_titles.include(alternative);
}, },
getTitle: function(){
var self = this;
try {
return self.info.original_title ? self.info.original_title : self.info.titles[0];
}
catch(e){
return 'Unknown';
}
},
get: function(key){
return this.info[key]
},
showOptions: function(){ showOptions: function(){
var self = this; var self = this;
@@ -282,6 +292,8 @@ Block.Search.Item = new Class({
}) })
); );
self.mask.fade('out'); self.mask.fade('out');
self.fireEvent('added');
}, },
'onFailure': function(){ 'onFailure': function(){
self.options_el.empty(); self.options_el.empty();
+23 -26
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParams, getParam
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Profile, ProfileType, Movie from couchpotato.core.settings.model import Profile, ProfileType, Movie
@@ -46,12 +45,12 @@ class ProfilePlugin(Plugin):
movie.profile_id = default_profile.get('id') movie.profile_id = default_profile.get('id')
db.commit() db.commit()
def allView(self): def allView(self, **kwargs):
return jsonified({ return {
'success': True, 'success': True,
'list': self.all() 'list': self.all()
}) }
def all(self): def all(self):
@@ -62,32 +61,31 @@ class ProfilePlugin(Plugin):
for profile in profiles: for profile in profiles:
temp.append(profile.to_dict(self.to_dict)) temp.append(profile.to_dict(self.to_dict))
db.expire_all()
return temp return temp
def save(self): def save(self, **kwargs):
params = getParams()
db = get_session() db = get_session()
p = db.query(Profile).filter_by(id = params.get('id')).first() p = db.query(Profile).filter_by(id = kwargs.get('id')).first()
if not p: if not p:
p = Profile() p = Profile()
db.add(p) db.add(p)
p.label = toUnicode(params.get('label')) p.label = toUnicode(kwargs.get('label'))
p.order = params.get('order', p.order if p.order else 0) p.order = kwargs.get('order', p.order if p.order else 0)
p.core = params.get('core', False) p.core = kwargs.get('core', False)
#delete old types #delete old types
[db.delete(t) for t in p.types] [db.delete(t) for t in p.types]
order = 0 order = 0
for type in params.get('types', []): for type in kwargs.get('types', []):
t = ProfileType( t = ProfileType(
order = order, order = order,
finish = type.get('finish') if order > 0 else 1, finish = type.get('finish') if order > 0 else 1,
wait_for = params.get('wait_for'), wait_for = kwargs.get('wait_for'),
quality_id = type.get('quality_id') quality_id = type.get('quality_id')
) )
p.types.append(t) p.types.append(t)
@@ -98,10 +96,10 @@ class ProfilePlugin(Plugin):
profile_dict = p.to_dict(self.to_dict) profile_dict = p.to_dict(self.to_dict)
return jsonified({ return {
'success': True, 'success': True,
'profile': profile_dict 'profile': profile_dict
}) }
def default(self): def default(self):
@@ -109,30 +107,28 @@ class ProfilePlugin(Plugin):
default = db.query(Profile).first() default = db.query(Profile).first()
default_dict = default.to_dict(self.to_dict) default_dict = default.to_dict(self.to_dict)
db.expire_all()
return default_dict return default_dict
def saveOrder(self): def saveOrder(self, **kwargs):
params = getParams()
db = get_session() db = get_session()
order = 0 order = 0
for profile in params.get('ids', []): for profile in kwargs.get('ids', []):
p = db.query(Profile).filter_by(id = profile).first() p = db.query(Profile).filter_by(id = profile).first()
p.hide = params.get('hidden')[order] p.hide = kwargs.get('hidden')[order]
p.order = order p.order = order
order += 1 order += 1
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def delete(self): def delete(self, id = None, **kwargs):
id = getParam('id')
db = get_session() db = get_session()
@@ -151,10 +147,11 @@ class ProfilePlugin(Plugin):
except Exception, e: except Exception, e:
message = log.error('Failed deleting Profile: %s', e) message = log.error('Failed deleting Profile: %s', e)
return jsonified({ db.expire_all()
return {
'success': success, 'success': success,
'message': message 'message': message
}) }
def fill(self): def fill(self):
@@ -13,11 +13,11 @@
.profile > .delete { .profile > .delete {
position: absolute; position: absolute;
padding: 25px 20px; padding: 16px;
background-position: center;
right: 0; right: 0;
cursor: pointer; cursor: pointer;
opacity: 0.6; opacity: 0.6;
color: #fd5353;
} }
.profile > .delete:hover { .profile > .delete:hover {
opacity: 1; opacity: 1;
@@ -29,6 +29,7 @@
.profile .qualities { .profile .qualities {
min-height: 80px; min-height: 80px;
padding-top: 0;
} }
.profile .formHint { .profile .formHint {
@@ -97,11 +98,13 @@
} }
.profile .types .type .delete { .profile .types .type .delete {
background-position: left center;
height: 20px; height: 20px;
width: 20px; width: 20px;
line-height: 20px;
visibility: hidden; visibility: hidden;
cursor: pointer; cursor: pointer;
font-size: 13px;
color: #fd5353;
} }
.profile .types .type:hover:not(.is_empty) .delete { .profile .types .type:hover:not(.is_empty) .delete {
@@ -24,7 +24,7 @@ var Profile = new Class({
var data = self.data; var data = self.data;
self.el = new Element('div.profile').adopt( self.el = new Element('div.profile').adopt(
self.delete_button = new Element('span.delete.icon', { self.delete_button = new Element('span.delete.icon2', {
'events': { 'events': {
'click': self.del.bind(self) 'click': self.del.bind(self)
} }
@@ -256,7 +256,7 @@ Profile.Type = new Class({
} }
}) })
), ),
new Element('span.delete.icon', { new Element('span.delete.icon2', {
'events': { 'events': {
'click': self.del.bind(self) 'click': self.del.bind(self)
} }
+26 -30
View File
@@ -2,13 +2,11 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Quality, Profile, ProfileType from couchpotato.core.settings.model import Quality, Profile, ProfileType
from sqlalchemy.sql.expression import or_ from sqlalchemy.sql.expression import or_
import os.path
import re import re
import time import time
@@ -19,13 +17,13 @@ class QualityPlugin(Plugin):
qualities = [ qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']}, {'identifier': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']}, {'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']} {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
@@ -52,12 +50,12 @@ class QualityPlugin(Plugin):
def preReleases(self): def preReleases(self):
return self.pre_releases return self.pre_releases
def allView(self): def allView(self, **kwargs):
return jsonified({ return {
'success': True, 'success': True,
'list': self.all() 'list': self.all()
}) }
def all(self): def all(self):
@@ -89,20 +87,18 @@ class QualityPlugin(Plugin):
if identifier == q.get('identifier'): if identifier == q.get('identifier'):
return q return q
def saveSize(self): def saveSize(self, **kwargs):
params = getParams()
db = get_session() db = get_session()
quality = db.query(Quality).filter_by(identifier = params.get('identifier')).first() quality = db.query(Quality).filter_by(identifier = kwargs.get('identifier')).first()
if quality: if quality:
setattr(quality, params.get('value_type'), params.get('value')) setattr(quality, kwargs.get('value_type'), kwargs.get('value'))
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def fill(self): def fill(self):
@@ -164,7 +160,6 @@ class QualityPlugin(Plugin):
if cached and extra is {}: return cached if cached and extra is {}: return cached
for cur_file in files: for cur_file in files:
size = (os.path.getsize(cur_file) / 1024 / 1024) if os.path.isfile(cur_file) else 0
words = re.split('\W+', cur_file.lower()) words = re.split('\W+', cur_file.lower())
for quality in self.all(): for quality in self.all():
@@ -188,29 +183,30 @@ class QualityPlugin(Plugin):
return self.setCache(hash, quality) return self.setCache(hash, quality)
# Try again with loose testing # Try again with loose testing
quality = self.guessLoose(hash, extra = extra) quality = self.guessLoose(hash, files = files, extra = extra)
if quality: if quality:
return self.setCache(hash, quality) return self.setCache(hash, quality)
log.debug('Could not identify quality for: %s', files) log.debug('Could not identify quality for: %s', files)
return None return None
def guessLoose(self, hash, extra): def guessLoose(self, hash, files = None, extra = None):
for quality in self.all(): if extra:
for quality in self.all():
# Check width resolution, range 20 # Check width resolution, range 20
if (quality.get('width', 720) - 20) <= extra.get('resolution_width', 0) <= (quality.get('width', 720) + 20): if quality.get('width') and (quality.get('width') - 20) <= extra.get('resolution_width', 0) <= (quality.get('width') + 20):
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 720), extra.get('resolution_width', 0))) log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width'), extra.get('resolution_width', 0)))
return self.setCache(hash, quality) return self.setCache(hash, quality)
# Check height resolution, range 20 # Check height resolution, range 20
if (quality.get('height', 480) - 20) <= extra.get('resolution_height', 0) <= (quality.get('height', 480) + 20): if quality.get('height') and (quality.get('height') - 20) <= extra.get('resolution_height', 0) <= (quality.get('height') + 20):
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height', 480), extra.get('resolution_height', 0))) log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height'), extra.get('resolution_height', 0)))
return self.setCache(hash, quality) return self.setCache(hash, quality)
if 480 <= extra.get('resolution_width', 0) <= 720: if 480 <= extra.get('resolution_width', 0) <= 720:
log.debug('Found as dvdrip') log.debug('Found as dvdrip')
return self.setCache(hash, self.single('dvdrip')) return self.setCache(hash, self.single('dvdrip'))
return None return None
+15 -20
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.scanner.main import Scanner from couchpotato.core.plugins.scanner.main import Scanner
@@ -108,13 +107,11 @@ class Release(Plugin):
# Check database and update/insert if necessary # Check database and update/insert if necessary
return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True) return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True)
def deleteView(self): def deleteView(self, id = None, **kwargs):
release_id = getParam('id') return {
'success': self.delete(id)
return jsonified({ }
'success': self.delete(release_id)
})
def delete(self, id): def delete(self, id):
@@ -146,25 +143,23 @@ class Release(Plugin):
return False return False
def ignore(self): def ignore(self, id = None, **kwargs):
db = get_session() db = get_session()
id = getParam('id')
rel = db.query(Relea).filter_by(id = id).first() rel = db.query(Relea).filter_by(id = id).first()
if rel: if rel:
ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True) ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True)
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id') rel.status_id = available_status.get('id') if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status.get('id')
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def download(self): def download(self, id = None, **kwargs):
db = get_session() db = get_session()
id = getParam('id')
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True) snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
@@ -180,7 +175,7 @@ class Release(Plugin):
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
if item['type'] != 'torrent_magnet': if item['type'] != 'torrent_magnet':
item['download'] = provider.download item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({ success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},
@@ -199,12 +194,12 @@ class Release(Plugin):
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name']) fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return jsonified({ return {
'success': success 'success': success
}) }
else: else:
log.error('Couldn\'t find release with id: %s', id) log.error('Couldn\'t find release with id: %s', id)
return jsonified({ return {
'success': False 'success': False
}) }
@@ -14,10 +14,14 @@ rename_options = {
'year': 'Year (2011)', 'year': 'Year (2011)',
'first': 'First letter (M)', 'first': 'First letter (M)',
'quality': 'Quality (720p)', 'quality': 'Quality (720p)',
'quality_type': '(HD) or (SD)',
'video': 'Video (x264)', 'video': 'Video (x264)',
'audio': 'Audio (DTS)', 'audio': 'Audio (DTS)',
'group': 'Releasegroup name', 'group': 'Releasegroup name',
'source': 'Source media (Bluray)', 'source': 'Source media (Bluray)',
'resolution_width': 'resolution width (1280)',
'resolution_height': 'resolution height (720)',
'audio_channels': 'audio channels (7.1)',
'original': 'Original filename', 'original': 'Original filename',
'original_folder': 'Original foldername', 'original_folder': 'Original foldername',
'imdb_id': 'IMDB id (tt0123456)', 'imdb_id': 'IMDB id (tt0123456)',
+20 -17
View File
@@ -2,9 +2,8 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss from couchpotato.core.helpers.encoding import toUnicode, ss
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
getImdb, link, symlink getImdb, link, symlink, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, File, Profile, Release, \ from couchpotato.core.settings.model import Library, File, Profile, Release, \
@@ -25,10 +24,10 @@ class Renamer(Plugin):
checking_snatched = False checking_snatched = False
def __init__(self): def __init__(self):
addApiView('renamer.scan', self.scanView, docs = { addApiView('renamer.scan', self.scanView, docs = {
'desc': 'For the renamer to check for new files to rename in a folder', 'desc': 'For the renamer to check for new files to rename in a folder',
'params': { 'params': {
'async': {'desc': 'Optional: Set to 1 if you dont want to fire the renamer.scan asynchronous.'},
'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'}, 'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'},
'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'}, 'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'},
'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'}, 'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'},
@@ -59,21 +58,23 @@ class Renamer(Plugin):
return True return True
def scanView(self): def scanView(self, **kwargs):
params = getParams() async = tryInt(kwargs.get('async', None))
movie_folder = params.get('movie_folder', None) movie_folder = kwargs.get('movie_folder', None)
downloader = params.get('downloader', None) downloader = kwargs.get('downloader', None)
download_id = params.get('download_id', None) download_id = kwargs.get('download_id', None)
fireEventAsync('renamer.scan', fire_handle = fireEvent if not async else fireEventAsync
fire_handle('renamer.scan',
movie_folder = movie_folder, movie_folder = movie_folder,
download_info = {'id': download_id, 'downloader': downloader} if download_id else None download_info = {'id': download_id, 'downloader': downloader} if download_id else None
) )
return jsonified({ return {
'success': True 'success': True
}) }
def scan(self, movie_folder = None, download_info = None): def scan(self, movie_folder = None, download_info = None):
@@ -180,6 +181,7 @@ class Renamer(Plugin):
'source': group['meta_data']['source'], 'source': group['meta_data']['source'],
'resolution_width': group['meta_data'].get('resolution_width'), 'resolution_width': group['meta_data'].get('resolution_width'),
'resolution_height': group['meta_data'].get('resolution_height'), 'resolution_height': group['meta_data'].get('resolution_height'),
'audio_channels': group['meta_data'].get('audio_channels'),
'imdb_id': library['identifier'], 'imdb_id': library['identifier'],
'cd': '', 'cd': '',
'cd_nr': '', 'cd_nr': '',
@@ -204,6 +206,7 @@ class Renamer(Plugin):
cd = 1 if multiple else 0 cd = 1 if multiple else 0
for current_file in sorted(list(group['files'][file_type])): for current_file in sorted(list(group['files'][file_type])):
current_file = toUnicode(current_file)
# Original filename # Original filename
replacements['original'] = os.path.splitext(os.path.basename(current_file))[0] replacements['original'] = os.path.splitext(os.path.basename(current_file))[0]
@@ -217,15 +220,15 @@ class Renamer(Plugin):
replacements['cd_nr'] = cd if multiple else '' replacements['cd_nr'] = cd if multiple else ''
# Naming # Naming
final_folder_name = self.doReplace(folder_name, replacements).lstrip('. ') final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements).lstrip('. ') final_file_name = self.doReplace(file_name, replacements)
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)] replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
# Meta naming # Meta naming
if file_type is 'trailer': if file_type is 'trailer':
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True).lstrip('. ') final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True)
elif file_type is 'nfo': elif file_type is 'nfo':
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True).lstrip('. ') final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True)
# Seperator replace # Seperator replace
if separator: if separator:
@@ -279,7 +282,7 @@ class Renamer(Plugin):
# Don't add language if multiple languages in 1 subtitle file # Don't add language if multiple languages in 1 subtitle file
if len(sub_langs) == 1: if len(sub_langs) == 1:
sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext'])) sub_name = sub_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name) rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
rename_files = mergeDicts(rename_files, rename_extras) rename_files = mergeDicts(rename_files, rename_extras)
@@ -553,7 +556,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced) replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced)
sep = self.conf('separator') sep = self.conf('separator')
return self.replaceDoubles(replaced).replace(' ', ' ' if not sep else sep) return self.replaceDoubles(replaced.lstrip('. ')).replace(' ', ' ' if not sep else sep)
def replaceDoubles(self, string): def replaceDoubles(self, string):
return string.replace(' ', ' ').replace(' .', '.') return string.replace(' ', ' ').replace(' .', '.')
+14 -7
View File
@@ -336,7 +336,7 @@ class Scanner(Plugin):
break break
if return_ignored is False and identifier in ignored_identifiers: if return_ignored is False and identifier in ignored_identifiers:
log.debug('Ignore file found, ignoring release: %s' % identifier) log.debug('Ignore file found, ignoring release: %s', identifier)
continue continue
# Group extra (and easy) files first # Group extra (and easy) files first
@@ -385,6 +385,8 @@ class Scanner(Plugin):
for file_type in group['files']: for file_type in group['files']:
if not file_type is 'leftover': if not file_type is 'leftover':
group['files']['leftover'] -= set(group['files'][file_type]) group['files']['leftover'] -= set(group['files'][file_type])
group['files'][file_type] = list(group['files'][file_type])
group['files']['leftover'] = list(group['files']['leftover'])
# Delete the unsorted list # Delete the unsorted list
del group['unsorted_files'] del group['unsorted_files']
@@ -430,6 +432,7 @@ class Scanner(Plugin):
data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio'])) data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio']))
data['resolution_width'] = meta.get('resolution_width', 720) data['resolution_width'] = meta.get('resolution_width', 720)
data['resolution_height'] = meta.get('resolution_height', 480) data['resolution_height'] = meta.get('resolution_height', 480)
data['audio_channels'] = meta.get('audio_channels', 2.0)
data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480) data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480)
except: except:
log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc())) log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
@@ -438,11 +441,14 @@ class Scanner(Plugin):
if data.get('audio'): break if data.get('audio'): break
# Use the quality guess first, if that failes use the quality we wanted to download # Use the quality guess first, if that failes use the quality we wanted to download
data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True) data['quality'] = None
if download_info and download_info.get('quality'):
data['quality'] = fireEvent('quality.single', download_info.get('quality'), single = True)
if not data['quality']: if not data['quality']:
if download_info and download_info.get('quality'): data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
data['quality'] = fireEvent('quality.single', download_info.get('quality'), single = True)
else: if not data['quality']:
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True) data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD' data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
@@ -471,6 +477,7 @@ class Scanner(Plugin):
'audio': ac, 'audio': ac,
'resolution_width': tryInt(p.video[0].width), 'resolution_width': tryInt(p.video[0].width),
'resolution_height': tryInt(p.video[0].height), 'resolution_height': tryInt(p.video[0].height),
'audio_channels': p.audio[0].channels,
} }
except ParseError: except ParseError:
log.debug('Failed to parse meta for %s', filename) log.debug('Failed to parse meta for %s', filename)
@@ -577,7 +584,7 @@ class Scanner(Plugin):
movie = fireEvent('movie.by_hash', file = cur_file, merge = True) movie = fireEvent('movie.by_hash', file = cur_file, merge = True)
if len(movie) > 0: if len(movie) > 0:
imdb_id = movie[0]['imdb'] imdb_id = movie[0].get('imdb')
if imdb_id: if imdb_id:
log.debug('Found movie via OpenSubtitleHash: %s', cur_file) log.debug('Found movie via OpenSubtitleHash: %s', cur_file)
break break
@@ -595,7 +602,7 @@ class Scanner(Plugin):
movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1) movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1)
if len(movie) > 0: if len(movie) > 0:
imdb_id = movie[0]['imdb'] imdb_id = movie[0].get('imdb')
log.debug('Found movie via search: %s', cur_file) log.debug('Found movie via search: %s', cur_file)
if imdb_id: break if imdb_id: break
else: else:
+1 -1
View File
@@ -18,7 +18,7 @@ class Score(Plugin):
def calculate(self, nzb, movie): def calculate(self, nzb, movie):
''' Calculate the score of a NZB, used for sorting later ''' ''' Calculate the score of a NZB, used for sorting later '''
score = nameScore(toUnicode(nzb['name']), movie['library']['year']) score = nameScore(toUnicode(nzb['name'] + ' ' + nzb.get('name_extra', '')), movie['library']['year'])
for movie_title in movie['library']['titles']: for movie_title in movie['library']['titles']:
score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title'])) score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
+3 -5
View File
@@ -1,6 +1,6 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.environment import Env from couchpotato.environment import Env
import re import re
@@ -42,10 +42,8 @@ def nameScore(name, year):
# Contains preferred word # Contains preferred word
nzb_words = re.split('\W+', simplifyString(name)) nzb_words = re.split('\W+', simplifyString(name))
preferred_words = [x.strip() for x in Env.setting('preferred_words', section = 'searcher').split(',')] preferred_words = splitString(Env.setting('preferred_words', section = 'searcher'))
for word in preferred_words: score += 100 * len(list(set(nzb_words) & set(preferred_words)))
if word.strip() and word.strip().lower() in nzb_words:
score = score + 100
return score return score
@@ -57,6 +57,14 @@ config = [{
'advanced': True, 'advanced': True,
'description': 'Cron settings for the searcher see: <a href="http://packages.python.org/APScheduler/cronschedule.html">APScheduler</a> for details.', 'description': 'Cron settings for the searcher see: <a href="http://packages.python.org/APScheduler/cronschedule.html">APScheduler</a> for details.',
'options': [ 'options': [
{
'name': 'run_on_launch',
'label': 'Run on launch',
'advanced': True,
'default': 0,
'type': 'bool',
'description': 'Force run the searcher after (re)start.',
},
{ {
'name': 'cron_day', 'name': 'cron_day',
'label': 'Day', 'label': 'Day',
+30 -23
View File
@@ -2,13 +2,13 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import simplifyString, toUnicode from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \ from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
possibleTitles possibleTitles
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env from couchpotato.environment import Env
from datetime import date
from inspect import ismethod, isfunction from inspect import ismethod, isfunction
from sqlalchemy.exc import InterfaceError from sqlalchemy.exc import InterfaceError
import datetime import datetime
@@ -50,6 +50,9 @@ class Searcher(Plugin):
}"""}, }"""},
}) })
if self.conf('run_on_launch'):
addEvent('app.load', self.allMovies)
addEvent('app.load', self.setCrons) addEvent('app.load', self.setCrons)
addEvent('setting.save.searcher.cron_day.after', self.setCrons) addEvent('setting.save.searcher.cron_day.after', self.setCrons)
addEvent('setting.save.searcher.cron_hour.after', self.setCrons) addEvent('setting.save.searcher.cron_hour.after', self.setCrons)
@@ -58,7 +61,7 @@ class Searcher(Plugin):
def setCrons(self): def setCrons(self):
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
def allMoviesView(self): def allMoviesView(self, **kwargs):
in_progress = self.in_progress in_progress = self.in_progress
if not in_progress: if not in_progress:
@@ -67,15 +70,15 @@ class Searcher(Plugin):
else: else:
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress') fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
return jsonified({ return {
'success': not in_progress 'success': not in_progress
}) }
def getProgress(self): def getProgress(self, **kwargs):
return jsonified({ return {
'progress': self.in_progress 'progress': self.in_progress
}) }
def allMovies(self): def allMovies(self):
@@ -146,9 +149,10 @@ class Searcher(Plugin):
pre_releases = fireEvent('quality.pre_releases', single = True) pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True) release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
available_status, ignored_status = fireEvent('status.get', ['available', 'ignored'], single = True) available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
found_releases = [] found_releases = []
too_early_to_search = []
default_title = getTitle(movie['library']) default_title = getTitle(movie['library'])
if not default_title: if not default_title:
@@ -161,15 +165,15 @@ class Searcher(Plugin):
ret = False ret = False
for quality_type in movie['profile']['types']: for quality_type in movie['profile']['types']:
if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates): if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title)) too_early_to_search.append(quality_type['quality']['identifier'])
continue continue
has_better_quality = 0 has_better_quality = 0
# See if better quality is available # See if better quality is available
for release in movie['releases']: for release in movie['releases']:
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]: if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]:
has_better_quality += 1 has_better_quality += 1
# Don't search for quality lower then already available. # Don't search for quality lower then already available.
@@ -240,7 +244,7 @@ class Searcher(Plugin):
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name'])) log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
continue continue
if nzb['status_id'] == ignored_status.get('id'): if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
log.info('Ignored: %s', nzb['name']) log.info('Ignored: %s', nzb['name'])
continue continue
@@ -269,6 +273,9 @@ class Searcher(Plugin):
if self.shuttingDown() or ret: if self.shuttingDown() or ret:
break break
if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True) fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
return ret return ret
@@ -392,7 +399,7 @@ class Searcher(Plugin):
req_match += len(list(set(nzb_words) & set(req))) == len(req) req_match += len(list(set(nzb_words) & set(req))) == len(req)
if self.conf('required_words') and req_match == 0: if self.conf('required_words') and req_match == 0:
log.info2("Wrong: Required word missing: %s" % nzb['name']) log.info2('Wrong: Required word missing: %s', nzb['name'])
return False return False
# Ignore releases # Ignore releases
@@ -403,7 +410,7 @@ class Searcher(Plugin):
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored) ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
if self.conf('ignored_words') and ignored_match: if self.conf('ignored_words') and ignored_match:
log.info2("Wrong: '%s' contains 'ignored words'" % (nzb['name'])) log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name']))
return False return False
# Ignore porn stuff # Ignore porn stuff
@@ -462,7 +469,7 @@ class Searcher(Plugin):
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0): if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
return True return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year'])) log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year']))
return False return False
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}): def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}):
@@ -552,11 +559,12 @@ class Searcher(Plugin):
return False return False
def couldBeReleased(self, is_pre_release, dates): def couldBeReleased(self, is_pre_release, dates, year = None):
now = int(time.time()) now = int(time.time())
now_year = date.today().year
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0): if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)):
return True return True
else: else:
@@ -586,18 +594,17 @@ class Searcher(Plugin):
return False return False
def tryNextReleaseView(self): def tryNextReleaseView(self, id = None, **kwargs):
trynext = self.tryNextRelease(getParam('id')) trynext = self.tryNextRelease(id)
return jsonified({ return {
'success': trynext 'success': trynext
}) }
def tryNextRelease(self, movie_id, manual = False): def tryNextRelease(self, movie_id, manual = False):
snatched_status = fireEvent('status.get', 'snatched', single = True) snatched_status, ignored_status = fireEvent('status.get', ['snatched', 'ignored'], single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
try: try:
db = get_session() db = get_session()
+3 -4
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Status from couchpotato.core.settings.model import Status
@@ -42,12 +41,12 @@ class StatusPlugin(Plugin):
}"""} }"""}
}) })
def list(self): def list(self, **kwargs):
return jsonified({ return {
'success': True, 'success': True,
'list': self.all() 'list': self.all()
}) }
def getById(self, id): def getById(self, id):
db = get_session() db = get_session()
+1 -1
View File
@@ -59,7 +59,7 @@ class Subtitle(Plugin):
for d_sub in downloaded: for d_sub in downloaded:
log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files)) log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files))
group['files']['subtitle'].add(d_sub.path) group['files']['subtitle'].append(d_sub.path)
group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2] group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2]
return True return True
+79 -10
View File
@@ -1,22 +1,91 @@
from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import splitString, md5
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie
from couchpotato.environment import Env
from sqlalchemy.sql.expression import or_
class Suggestion(Plugin): class Suggestion(Plugin):
def __init__(self): def __init__(self):
addApiView('suggestion.view', self.getView) addApiView('suggestion.view', self.suggestView)
addApiView('suggestion.ignore', self.ignoreView)
def getView(self): def suggestView(self, **kwargs):
limit_offset = getParam('limit_offset', None) movies = splitString(kwargs.get('movies', ''))
total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True) ignored = splitString(kwargs.get('ignored', ''))
limit = kwargs.get('limit', 6)
return jsonified({ if not movies or len(movies) == 0:
db = get_session()
active_movies = db.query(Movie) \
.filter(or_(*[Movie.status.has(identifier = s) for s in ['active', 'done']])).all()
movies = [x.library.identifier for x in active_movies]
if not ignored or len(ignored) == 0:
ignored = splitString(Env.prop('suggest_ignore', default = ''))
cached_suggestion = self.getCache('suggestion_cached')
if cached_suggestion:
suggestions = cached_suggestion
else:
suggestions = fireEvent('movie.suggest', movies = movies, ignore = ignored, single = True)
self.setCache(md5(ss('suggestion_cached')), suggestions, timeout = 6048000) # Cache for 10 weeks
return {
'success': True, 'success': True,
'empty': len(movies) == 0, 'count': len(suggestions),
'total': total_movies, 'suggestions': suggestions[:limit]
'movies': movies, }
})
def ignoreView(self, imdb = None, limit = 6, remove_only = False, **kwargs):
ignored = splitString(Env.prop('suggest_ignore', default = ''))
if imdb:
if not remove_only:
ignored.append(imdb)
Env.prop('suggest_ignore', ','.join(set(ignored)))
new_suggestions = self.updateSuggestionCache(ignore_imdb = imdb, limit = limit, ignored = ignored)
return {
'result': True,
'ignore_count': len(ignored),
'suggestions': new_suggestions[limit - 1:limit]
}
def updateSuggestionCache(self, ignore_imdb = None, limit = 6, ignored = None):
# Combine with previous suggestion_cache
cached_suggestion = self.getCache('suggestion_cached')
new_suggestions = []
ignored = [] if not ignored else ignored
if ignore_imdb:
for cs in cached_suggestion:
if cs.get('imdb') != ignore_imdb:
new_suggestions.append(cs)
# Get new results and add them
if len(new_suggestions) - 1 < limit:
db = get_session()
active_movies = db.query(Movie) \
.filter(or_(*[Movie.status.has(identifier = s) for s in ['active', 'done']])).all()
movies = [x.library.identifier for x in active_movies]
ignored.extend([x.get('imdb') for x in cached_suggestion])
suggestions = fireEvent('movie.suggest', movies = movies, ignore = list(set(ignored)), single = True)
if suggestions:
new_suggestions.extend(suggestions)
self.setCache(md5(ss('suggestion_cached')), new_suggestions, timeout = 6048000)
return new_suggestions
@@ -0,0 +1,84 @@
.suggestions {
}
.suggestions > h2 {
height: 40px;
}
.suggestions .movie_result {
display: inline-block;
width: 33.333%;
height: 150px;
}
@media all and (max-width: 960px) {
.suggestions .movie_result {
width: 50%;
}
}
@media all and (max-width: 600px) {
.suggestions .movie_result {
width: 100%;
}
}
.suggestions .movie_result .data {
left: 100px;
background: #4e5969;
border: none;
}
.suggestions .movie_result .data .info {
top: 15px;
left: 15px;
right: 15px;
}
.suggestions .movie_result .data .info h2 {
white-space: normal;
max-height: 120px;
font-size: 18px;
line-height: 18px;
}
.suggestions .movie_result .data .info .year {
position: static;
display: block;
margin: 5px 0 0;
padding: 0;
opacity: .6;
}
.suggestions .movie_result .data {
cursor: default;
}
.suggestions .movie_result .options {
left: 100px;
}
.suggestions .movie_result .thumbnail {
width: 100px;
}
.suggestions .movie_result .actions {
position: absolute;
bottom: 10px;
right: 10px;
display: none;
width: 120px;
}
.suggestions .movie_result:hover .actions {
display: block;
}
.suggestions .movie_result .data.open .actions {
display: none;
}
.suggestions .movie_result .actions a {
margin-left: 10px;
vertical-align: middle;
}
@@ -0,0 +1,102 @@
var SuggestList = new Class({
Implements: [Options, Events],
initialize: function(options){
var self = this;
self.setOptions(options);
self.create();
},
create: function(){
var self = this;
self.el = new Element('div.suggestions', {
'events': {
'click:relay(a.delete)': function(e, el){
(e).stop();
$(el).getParent('.movie_result').destroy();
Api.request('suggestion.ignore', {
'data': {
'imdb': el.get('data-ignore')
},
'onComplete': self.fill.bind(self)
});
}
}
}).grab(
new Element('h2', {
'text': 'You might like these'
})
);
self.api_request = Api.request('suggestion.view', {
'onComplete': self.fill.bind(self)
});
},
fill: function(json){
var self = this;
Object.each(json.suggestions, function(movie){
var m = new Block.Search.Item(movie, {
'onAdded': function(){
self.afterAdded(m, movie)
}
});
m.data_container.grab(
new Element('div.actions').adopt(
new Element('a.add.icon2', {
'title': 'Add movie with your default quality',
'data-add': movie.imdb,
'events': {
'click': m.showOptions.bind(m)
}
}),
$(new MA.IMDB(m)),
$(new MA.Trailer(m, {
'height': 150
})),
new Element('a.delete.icon2', {
'title': 'Don\'t suggest this movie again',
'data-ignore': movie.imdb
})
)
);
m.data_container.removeEvents('click');
$(m).inject(self.el);
});
},
afterAdded: function(m, movie){
var self = this;
setTimeout(function(){
$(m).destroy();
Api.request('suggestion.ignore', {
'data': {
'imdb': movie.imdb,
'remove_only': true
},
'onComplete': self.fill.bind(self)
});
}, 3000);
},
toElement: function(){
return this.el;
}
})
@@ -1,5 +1,7 @@
var includes = {{includes|tojson}}; {% autoescape None %}
var excludes = {{excludes|tojson}};
var includes = {{ json_encode(includes) }};
var excludes = {{ json_encode(excludes) }};
var specialChars = '\\{}+.():-|^$'; var specialChars = '\\{}+.():-|^$';
var makeRegex = function(pattern) { var makeRegex = function(pattern) {
@@ -20,6 +22,8 @@ var makeRegex = function(pattern) {
var isCorrectUrl = function() { var isCorrectUrl = function() {
for(i in includes) { for(i in includes) {
if(!includes.hasOwnProperty(i)) continue;
var reg = includes[i] var reg = includes[i]
if (makeRegex(reg).test(document.location.href)) if (makeRegex(reg).test(document.location.href))
return true; return true;
+35 -28
View File
@@ -1,13 +1,11 @@
from couchpotato import index
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.helpers.variable import isDict from couchpotato.core.helpers.variable import isDict
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.globals import request from tornado.web import RequestHandler
from flask.helpers import url_for
from flask.templating import render_template
import os import os
log = CPLog(__name__) log = CPLog(__name__)
@@ -18,7 +16,8 @@ class Userscript(Plugin):
version = 3 version = 3
def __init__(self): def __init__(self):
addApiView('userscript.get/<random>/<path:filename>', self.getUserScript, static = True) addApiView('userscript.get/(.*)/(.*)', self.getUserScript, static = True)
addApiView('userscript', self.iFrame) addApiView('userscript', self.iFrame)
addApiView('userscript.add_via_url', self.getViaUrl) addApiView('userscript.add_via_url', self.getViaUrl)
addApiView('userscript.includes', self.getIncludes) addApiView('userscript.includes', self.getIncludes)
@@ -26,38 +25,46 @@ class Userscript(Plugin):
addEvent('userscript.get_version', self.getVersion) addEvent('userscript.get_version', self.getVersion)
def bookmark(self): def bookmark(self, host = None, **kwargs):
params = { params = {
'includes': fireEvent('userscript.get_includes', merge = True), 'includes': fireEvent('userscript.get_includes', merge = True),
'excludes': fireEvent('userscript.get_excludes', merge = True), 'excludes': fireEvent('userscript.get_excludes', merge = True),
'host': getParam('host', None), 'host': host,
} }
return self.renderTemplate(__file__, 'bookmark.js', **params) return self.renderTemplate(__file__, 'bookmark.js', **params)
def getIncludes(self): def getIncludes(self, **kwargs):
return jsonified({ return {
'includes': fireEvent('userscript.get_includes', merge = True), 'includes': fireEvent('userscript.get_includes', merge = True),
'excludes': fireEvent('userscript.get_excludes', merge = True), 'excludes': fireEvent('userscript.get_excludes', merge = True),
})
def getUserScript(self, random = '', filename = ''):
params = {
'includes': fireEvent('userscript.get_includes', merge = True),
'excludes': fireEvent('userscript.get_excludes', merge = True),
'version': self.getVersion(),
'api': '%suserscript/' % url_for('api.index').lstrip('/'),
'host': request.host_url,
} }
script = self.renderTemplate(__file__, 'template.js', **params) def getUserScript(self, route, **kwargs):
self.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
klass = self
class UserscriptHandler(RequestHandler):
def get(self, random, route):
params = {
'includes': fireEvent('userscript.get_includes', merge = True),
'excludes': fireEvent('userscript.get_excludes', merge = True),
'version': klass.getVersion(),
'api': '%suserscript/' % Env.get('api_base'),
'host': '%s://%s' % (self.request.protocol, self.request.host),
}
script = klass.renderTemplate(__file__, 'template.js', **params)
klass.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
self.redirect(Env.get('api_base') + 'file.cache/couchpotato.user.js')
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), UserscriptHandler)])
from flask.helpers import send_from_directory
return send_from_directory(Env.get('cache_dir'), 'couchpotato.user.js')
def getVersion(self): def getVersion(self):
@@ -69,12 +76,12 @@ class Userscript(Plugin):
return version return version
def iFrame(self): def iFrame(self, **kwargs):
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) return index()
def getViaUrl(self): def getViaUrl(self, url = None, **kwargs):
url = getParam('url') print url
params = { params = {
'url': url, 'url': url,
@@ -84,4 +91,4 @@ class Userscript(Plugin):
log.error('Failed adding movie via url: %s', url) log.error('Failed adding movie via url: %s', url)
params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info' params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info'
return jsonified(params) return params
@@ -9,15 +9,16 @@
// @grant none // @grant none
// @version {{version}} // @version {{version}}
// @match {{host}}* // @match {{host}}/*
{% for include in includes %} {% for include in includes %}
// @match {{include}}{% endfor %} // @match {{include}}{% end %}
{% for exclude in excludes %} {% for exclude in excludes %}
// @exclude {{exclude}}{% endfor %} // @exclude {{exclude}}{% end %}
// @exclude {{host}}{{api.rstrip('/')}}* // @exclude {{host}}{{api.rstrip('/')}}*
// ==/UserScript== // ==/UserScript==
{% autoescape None %}
if (window.top == window.self){ // Only run on top window if (window.top == window.self){ // Only run on top window
var version = {{version}}, var version = {{version}},
@@ -1,6 +0,0 @@
from .main import V1Importer
def start():
return V1Importer()
config = []
@@ -1,30 +0,0 @@
<html>
<head>
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/main.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.generic.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.css') }}" type="text/css">
<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/mootools.js') }}"></script>
<script type="text/javascript">
window.addEvent('domready', function(){
if($('old_db'))
$('old_db').addEvent('change', function(){
$('form').submit();
});
});
</script>
</head>
<body>
{% if message: %}
{{ message }}
{% else: %}
<form id="form" method="post" enctype="multipart/form-data">
<input type="file" name="old_db" id="old_db" />
</form>
{% endif %}
</body>
</html>
@@ -1,56 +0,0 @@
from couchpotato.api import addApiView
from couchpotato.core.event import fireEventAsync
from couchpotato.core.helpers.variable import getImdb
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from flask.globals import request
from flask.helpers import url_for
import os
log = CPLog(__name__)
class V1Importer(Plugin):
def __init__(self):
addApiView('v1.import', self.fromOld, methods = ['GET', 'POST'])
def fromOld(self):
if request.method != 'POST':
return self.renderTemplate(__file__, 'form.html', url_for = url_for)
file = request.files['old_db']
uploaded_file = os.path.join(Env.get('cache_dir'), 'v1_database.db')
if os.path.isfile(uploaded_file):
os.remove(uploaded_file)
file.save(uploaded_file)
try:
import sqlite3
conn = sqlite3.connect(uploaded_file)
wanted = []
t = ('want',)
cur = conn.execute('SELECT status, imdb FROM Movie WHERE status=?', t)
for row in cur:
status, imdb = row
if getImdb(imdb):
wanted.append(imdb)
conn.close()
wanted = set(wanted)
for imdb in wanted:
fireEventAsync('movie.add', {'identifier': imdb}, search_after = False)
message = 'Successfully imported %s movie(s)' % len(wanted)
except Exception, e:
message = 'Failed: %s' % e
return self.renderTemplate(__file__, 'form.html', url_for = url_for, message = message)
@@ -11,8 +11,9 @@ log = CPLog(__name__)
class Automation(Provider): class Automation(Provider):
enabled_option = 'automation_enabled' enabled_option = 'automation_enabled'
http_time_between_calls = 2
interval = 86400 interval = 1800
last_checked = 0 last_checked = 0
def __init__(self): def __init__(self):
@@ -50,6 +51,7 @@ class Automation(Provider):
def isMinimalMovie(self, movie): def isMinimalMovie(self, movie):
if not movie.get('rating'): if not movie.get('rating'):
log.info('ignoring %s as no rating is available for.', (movie['original_title']))
return False return False
if movie['rating'] and movie['rating'].get('imdb'): if movie['rating'] and movie['rating'].get('imdb'):
@@ -73,13 +75,13 @@ class Automation(Provider):
req_match += len(list(set(movie_genres) & set(req))) == len(req) req_match += len(list(set(movie_genres) & set(req))) == len(req)
if self.getMinimal('required_genres') and req_match == 0: if self.getMinimal('required_genres') and req_match == 0:
log.info2("Required genre(s) missing for %s" % movie['original_title']) log.info2('Required genre(s) missing for %s', movie['original_title'])
return False return False
for ign_set in ignored_genres: for ign_set in ignored_genres:
ign = splitString(ign_set, '&') ign = splitString(ign_set, '&')
if len(list(set(movie_genres) & set(ign))) == len(ign): if len(list(set(movie_genres) & set(ign))) == len(ign):
log.info2("%s has blacklisted genre(s): %s" % (movie['original_title'], ign)) log.info2('%s has blacklisted genre(s): %s', (movie['original_title'], ign))
return False return False
return True return True
@@ -1,13 +1,15 @@
from bs4 import BeautifulSoup
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation from couchpotato.core.providers.automation.base import Automation
from bs4 import BeautifulSoup
log = CPLog(__name__) log = CPLog(__name__)
class Goodfilms(Automation): class Goodfilms(Automation):
url = 'http://goodfil.ms/%s/queue' url = 'http://goodfil.ms/%s/queue?page=%d&without_layout=1'
interval = 1800
def getIMDBids(self): def getIMDBids(self):
@@ -25,12 +27,25 @@ class Goodfilms(Automation):
def getWatchlist(self): def getWatchlist(self):
url = self.url % self.conf('automation_username')
soup = BeautifulSoup(self.getHTMLData(url))
movies = [] movies = []
page = 1
for movie in soup.find_all('div', attrs = { 'class': 'movie', 'data-film-title': True }): while True:
movies.append({ 'title': movie['data-film-title'], 'year': movie['data-film-year'] }) url = self.url % (self.conf('automation_username'), page)
data = self.getHTMLData(url)
soup = BeautifulSoup(data)
this_watch_list = soup.find_all('div', attrs = { 'class': 'movie', 'data-film-title': True })
if not this_watch_list: # No Movies
break
for movie in this_watch_list:
movies.append({ 'title': movie['data-film-title'], 'year': movie['data-film-year'] })
if not 'next page' in data.lower():
break
page += 1
return movies return movies
@@ -11,7 +11,7 @@ config = [{
'list': 'watchlist_providers', 'list': 'watchlist_providers',
'name': 'imdb_automation', 'name': 'imdb_automation',
'label': 'IMDB', 'label': 'IMDB',
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.', 'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the CSV link.',
'options': [ 'options': [
{ {
'name': 'automation_enabled', 'name': 'automation_enabled',
@@ -12,6 +12,8 @@ class Letterboxd(Automation):
url = 'http://letterboxd.com/%s/watchlist/' url = 'http://letterboxd.com/%s/watchlist/'
pattern = re.compile(r'(.*)\((\d*)\)') pattern = re.compile(r'(.*)\((\d*)\)')
interval = 1800
def getIMDBids(self): def getIMDBids(self):
urls = splitString(self.conf('automation_urls')) urls = splitString(self.conf('automation_urls'))
@@ -35,10 +35,10 @@ class Rottentomatoes(Automation, RSS):
name = result.group(0) name = result.group(0)
if rating < tryInt(self.conf('tomatometer_percent')): if rating < tryInt(self.conf('tomatometer_percent')):
log.info2('%s seems to be rotten...' % name) log.info2('%s seems to be rotten...', name)
else: else:
log.info2('Found %s fresh enough movies, enqueuing: %s' % (rating, name)) log.info2('Found %s fresh enough movies, enqueuing: %s', (rating, name))
year = datetime.datetime.now().strftime("%Y") year = datetime.datetime.now().strftime("%Y")
imdb = self.search(name, year) imdb = self.search(name, year)
+33 -13
View File
@@ -62,7 +62,7 @@ class Provider(Plugin):
cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {}))) cache_key = '%s%s' % (md5(url), md5('%s' % kwargs.get('params', {})))
data = self.getCache(cache_key, url, **kwargs) data = self.getCache(cache_key, url, **kwargs)
if data: if data and len(data) > 0:
try: try:
data = XMLTree.fromstring(data) data = XMLTree.fromstring(data)
return self.getElements(data, item_path) return self.getElements(data, item_path)
@@ -86,6 +86,7 @@ class YarrProvider(Provider):
sizeKb = ['kb', 'kib'] sizeKb = ['kb', 'kib']
login_opener = None login_opener = None
last_login_check = 0
def __init__(self): def __init__(self):
addEvent('provider.enabled_types', self.getEnabledProviderType) addEvent('provider.enabled_types', self.getEnabledProviderType)
@@ -101,30 +102,49 @@ class YarrProvider(Provider):
def login(self): def login(self):
# Check if we are still logged in every hour
now = time.time()
if self.login_opener and self.last_login_check < (now - 3600):
try:
output = self.urlopen(self.urls['login_check'], opener = self.login_opener)
if self.loginCheckSuccess(output):
self.last_login_check = now
return True
else:
self.login_opener = None
except:
self.login_opener = None
if self.login_opener:
return True
try: try:
cookiejar = cookielib.CookieJar() cookiejar = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar)) opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
opener.addheaders = [] output = self.urlopen(self.urls['login'], params = self.getLoginParams(), opener = opener)
urllib2.install_opener(opener)
log.info2('Logging into %s', self.urls['login'])
f = opener.open(self.urls['login'], self.getLoginParams())
output = f.read()
f.close()
if self.loginSuccess(output): if self.loginSuccess(output):
self.last_login_check = now
self.login_opener = opener self.login_opener = opener
return True return True
except:
log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc()))
error = 'unknown'
except:
error = traceback.format_exc()
self.login_opener = None
log.error('Failed to login %s: %s', (self.getName(), error))
return False return False
def loginSuccess(self, output): def loginSuccess(self, output):
return True return True
def loginCheckSuccess(self, output):
return True
def loginDownload(self, url = '', nzb_id = ''): def loginDownload(self, url = '', nzb_id = ''):
try: try:
if not self.login_opener and not self.login(): if not self.login():
log.error('Failed downloading from %s', self.getName()) log.error('Failed downloading from %s', self.getName())
return self.urlopen(url, opener = self.login_opener) return self.urlopen(url, opener = self.login_opener)
except: except:
@@ -147,7 +167,7 @@ class YarrProvider(Provider):
return [] return []
# Login if needed # Login if needed
if self.urls.get('login') and (not self.login_opener and not self.login()): if self.urls.get('login') and not self.login():
log.error('Failed to login to: %s', self.getName()) log.error('Failed to login to: %s', self.getName())
return [] return []
@@ -179,7 +199,7 @@ class YarrProvider(Provider):
if hostname in download_url: if hostname in download_url:
return self return self
except: except:
log.debug('Url % s doesn\'t belong to %s', (url, self.getName())) log.debug('Url %s doesn\'t belong to %s', (url, self.getName()))
return return
@@ -255,7 +275,7 @@ class ResultList(list):
'id': 0, 'id': 0,
'type': self.provider.type, 'type': self.provider.type,
'provider': self.provider.getName(), 'provider': self.provider.getName(),
'download': self.provider.download, 'download': self.provider.loginDownload if self.provider.urls.get('login') else self.provider.download,
'url': '', 'url': '',
'name': '', 'name': '',
'age': 0, 'age': 0,
@@ -4,6 +4,7 @@ from couchpotato.core.helpers.variable import mergeDicts, randomString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library from couchpotato.core.settings.model import Library
import copy
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -11,6 +12,24 @@ log = CPLog(__name__)
class MovieResultModifier(Plugin): class MovieResultModifier(Plugin):
default_info = {
'tmdb_id': 0,
'titles': [],
'original_title': '',
'year': 0,
'images': {
'poster': [],
'backdrop': [],
'poster_original': [],
'backdrop_original': []
},
'runtime': 0,
'plot': '',
'tagline': '',
'imdb': '',
'genres': [],
}
def __init__(self): def __init__(self):
addEvent('result.modify.movie.search', self.combineOnIMDB) addEvent('result.modify.movie.search', self.combineOnIMDB)
addEvent('result.modify.movie.info', self.checkLibrary) addEvent('result.modify.movie.info', self.checkLibrary)
@@ -67,6 +86,9 @@ class MovieResultModifier(Plugin):
return temp return temp
def checkLibrary(self, result): def checkLibrary(self, result):
result = mergeDicts(copy.deepcopy(self.default_info), copy.deepcopy(result))
if result and result.get('imdb'): if result and result.get('imdb'):
return mergeDicts(result, self.getLibraryTags(result['imdb'])) return mergeDicts(result, self.getLibraryTags(result['imdb']))
return result return result
@@ -1,11 +1,7 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.providers.movie.base import MovieProvider
from couchpotato.core.settings.model import Movie
from couchpotato.environment import Env from couchpotato.environment import Env
import time import time
@@ -15,13 +11,13 @@ log = CPLog(__name__)
class CouchPotatoApi(MovieProvider): class CouchPotatoApi(MovieProvider):
urls = { urls = {
'search': 'https://couchpota.to/api/search/%s/', 'search': 'https://api.couchpota.to/search/%s/',
'info': 'https://couchpota.to/api/info/%s/', 'info': 'https://api.couchpota.to/info/%s/',
'is_movie': 'https://couchpota.to/api/ismovie/%s/', 'is_movie': 'https://api.couchpota.to/ismovie/%s/',
'eta': 'https://couchpota.to/api/eta/%s/', 'eta': 'https://api.couchpota.to/eta/%s/',
'suggest': 'https://couchpota.to/api/suggest/', 'suggest': 'https://api.couchpota.to/suggest/',
'updater': 'https://couchpota.to/api/updater/?%s', 'updater': 'https://api.couchpota.to/updater/?%s',
'messages': 'https://couchpota.to/api/messages/?%s', 'messages': 'https://api.couchpota.to/messages/?%s',
} }
http_time_between_calls = 0 http_time_between_calls = 0
api_version = 1 api_version = 1
@@ -30,7 +26,7 @@ class CouchPotatoApi(MovieProvider):
addEvent('movie.info', self.getInfo, priority = 1) addEvent('movie.info', self.getInfo, priority = 1)
addEvent('movie.search', self.search, priority = 1) addEvent('movie.search', self.search, priority = 1)
addEvent('movie.release_date', self.getReleaseDate) addEvent('movie.release_date', self.getReleaseDate)
addEvent('movie.suggest', self.suggest) addEvent('movie.suggest', self.getSuggestions)
addEvent('movie.is_movie', self.isMovie) addEvent('movie.is_movie', self.isMovie)
addEvent('cp.source_url', self.getSourceUrl) addEvent('cp.source_url', self.getSourceUrl)
@@ -51,8 +47,8 @@ class CouchPotatoApi(MovieProvider):
'branch': branch, 'branch': branch,
}), headers = self.getRequestHeaders()) }), headers = self.getRequestHeaders())
def search(self, q, limit = 12): def search(self, q, limit = 5):
return self.getJsonData(self.urls['search'] % tryUrlencode(q), headers = self.getRequestHeaders()) return self.getJsonData(self.urls['search'] % tryUrlencode(q) + ('?limit=%s' % limit), headers = self.getRequestHeaders())
def isMovie(self, identifier = None): def isMovie(self, identifier = None):
@@ -71,7 +67,8 @@ class CouchPotatoApi(MovieProvider):
return return
result = self.getJsonData(self.urls['info'] % identifier, headers = self.getRequestHeaders()) result = self.getJsonData(self.urls['info'] % identifier, headers = self.getRequestHeaders())
if result: return result if result:
return dict((k, v) for k, v in result.iteritems() if v)
return {} return {}
@@ -83,34 +80,15 @@ class CouchPotatoApi(MovieProvider):
return dates return dates
def suggest(self, movies = [], ignore = []): def getSuggestions(self, movies = [], ignore = []):
suggestions = self.getJsonData(self.urls['suggest'], params = { suggestions = self.getJsonData(self.urls['suggest'], params = {
'movies': ','.join(movies), 'movies': ','.join(movies),
#'ignore': ','.join(ignore), 'ignore': ','.join(ignore),
}) }, headers = self.getRequestHeaders())
log.info('Found Suggestions for %s', (suggestions)) log.info('Found suggestions for %s movies, %s ignored', (len(movies), len(ignore)))
return suggestions return suggestions
def suggestView(self):
params = getParams()
movies = params.get('movies')
ignore = params.get('ignore', [])
if not movies:
db = get_session()
active_movies = db.query(Movie).filter(Movie.status.has(identifier = 'active')).all()
movies = [x.library.identifier for x in active_movies]
suggestions = self.suggest(movies, ignore)
return jsonified({
'success': True,
'count': len(suggestions),
'suggestions': suggestions
})
def getRequestHeaders(self): def getRequestHeaders(self):
return { return {
'X-CP-Version': fireEvent('app.version', single = True), 'X-CP-Version': fireEvent('app.version', single = True),
@@ -86,7 +86,7 @@ class OMDBAPI(MovieProvider):
movie_data = { movie_data = {
'via_imdb': True, 'via_imdb': True,
'titles': [movie.get('Title')] if movie.get('Title') else [], 'titles': [movie.get('Title')] if movie.get('Title') else [],
'original_title': movie.get('Title', ''), 'original_title': movie.get('Title'),
'images': { 'images': {
'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [], 'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [],
}, },
@@ -96,14 +96,15 @@ class OMDBAPI(MovieProvider):
}, },
'imdb': str(movie.get('imdbID', '')), 'imdb': str(movie.get('imdbID', '')),
'runtime': self.runtimeToMinutes(movie.get('Runtime', '')), 'runtime': self.runtimeToMinutes(movie.get('Runtime', '')),
'released': movie.get('Released', ''), 'released': movie.get('Released'),
'year': year if isinstance(year, (int)) else None, 'year': year if isinstance(year, (int)) else None,
'plot': movie.get('Plot', ''), 'plot': movie.get('Plot'),
'genres': splitString(movie.get('Genre', '')), 'genres': splitString(movie.get('Genre', '')),
'directors': splitString(movie.get('Director', '')), 'directors': splitString(movie.get('Director', '')),
'writers': splitString(movie.get('Writer', '')), 'writers': splitString(movie.get('Writer', '')),
'actors': splitString(movie.get('Actors', '')), 'actors': splitString(movie.get('Actors', '')),
} }
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
except: except:
log.error('Failed parsing IMDB API json: %s', traceback.format_exc()) log.error('Failed parsing IMDB API json: %s', traceback.format_exc())
@@ -2,7 +2,7 @@ from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.providers.movie.base import MovieProvider
from libs.themoviedb import tmdb from themoviedb import tmdb
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -37,7 +37,7 @@ class TheMovieDb(MovieProvider):
if raw: if raw:
try: try:
results = self.parseMovie(raw) results = self.parseMovie(raw)
log.info('Found: %s', results['titles'][0] + ' (' + str(results['year']) + ')') log.info('Found: %s', results['titles'][0] + ' (' + str(results.get('year', 0)) + ')')
self.setCache(cache_key, results) self.setCache(cache_key, results)
return results return results
@@ -81,7 +81,7 @@ class TheMovieDb(MovieProvider):
if nr == limit: if nr == limit:
break break
log.info('Found: %s', [result['titles'][0] + ' (' + str(result['year']) + ')' for result in results]) log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results])
self.setCache(cache_key, results) self.setCache(cache_key, results)
return results return results
@@ -170,11 +170,12 @@ class TheMovieDb(MovieProvider):
'runtime': movie.get('runtime'), 'runtime': movie.get('runtime'),
'released': movie.get('released'), 'released': movie.get('released'),
'year': year, 'year': year,
'plot': movie.get('overview', ''), 'plot': movie.get('overview'),
'tagline': '',
'genres': genres, 'genres': genres,
} }
movie_data = dict((k, v) for k, v in movie_data.iteritems() if v)
# Add alternative names # Add alternative names
for alt in ['original_name', 'alternative_name']: for alt in ['original_name', 'alternative_name']:
alt_name = toUnicode(movie.get(alt)) alt_name = toUnicode(movie.get(alt))
@@ -3,7 +3,6 @@ from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
from dateutil.parser import parse
import json import json
import traceback import traceback
@@ -17,6 +16,7 @@ class FTDWorld(NZBProvider):
'detail': 'http://ftdworld.net/spotinfo.php?id=%s', 'detail': 'http://ftdworld.net/spotinfo.php?id=%s',
'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s', 'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s',
'login': 'http://ftdworld.net/api/login.php', 'login': 'http://ftdworld.net/api/login.php',
'login_check': 'http://ftdworld.net/api/login.php',
} }
http_time_between_calls = 3 #seconds http_time_between_calls = 3 #seconds
@@ -59,7 +59,6 @@ class FTDWorld(NZBProvider):
'age': self.calculateAge(tryInt(item.get('Created'))), 'age': self.calculateAge(tryInt(item.get('Created'))),
'size': item.get('Size', 0), 'size': item.get('Size', 0),
'url': self.urls['download'] % nzb_id, 'url': self.urls['download'] % nzb_id,
'download': self.loginDownload,
'detail_url': self.urls['detail'] % nzb_id, 'detail_url': self.urls['detail'] % nzb_id,
'score': (tryInt(item.get('webPlus', 0)) - tryInt(item.get('webMin', 0))) * 3, 'score': (tryInt(item.get('webPlus', 0)) - tryInt(item.get('webMin', 0))) * 3,
}) })
@@ -79,3 +78,6 @@ class FTDWorld(NZBProvider):
return json.loads(output).get('goodToGo', False) return json.loads(output).get('goodToGo', False)
except: except:
return False return False
loginCheckSuccess = loginSuccess
+28 -7
View File
@@ -53,11 +53,20 @@ class Newznab(NZBProvider, RSS):
for nzb in nzbs: for nzb in nzbs:
date = None date = None
spotter = None
for item in nzb: for item in nzb:
if date and spotter:
break
if item.attrib.get('name') == 'usenetdate': if item.attrib.get('name') == 'usenetdate':
date = item.attrib.get('value') date = item.attrib.get('value')
break break
# Get the name of the person who posts the spot
if item.attrib.get('name') == 'poster':
if "@spot.net" in item.attrib.get('value'):
spotter = item.attrib.get('value').split("@")[0]
continue
if not date: if not date:
date = self.getTextElement(nzb, 'pubDate') date = self.getTextElement(nzb, 'pubDate')
@@ -67,10 +76,15 @@ class Newznab(NZBProvider, RSS):
if not name: if not name:
continue continue
name_extra = ''
if spotter:
name_extra = spotter
results.append({ results.append({
'id': nzb_id, 'id': nzb_id,
'provider_extra': urlparse(host['host']).hostname or host['host'], 'provider_extra': urlparse(host['host']).hostname or host['host'],
'name': self.getTextElement(nzb, 'title'), 'name': name,
'name_extra': name_extra,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024, 'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host), 'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
@@ -81,17 +95,24 @@ class Newznab(NZBProvider, RSS):
def getHosts(self): def getHosts(self):
uses = splitString(str(self.conf('use'))) uses = splitString(str(self.conf('use')), clean = False)
hosts = splitString(self.conf('host')) hosts = splitString(self.conf('host'), clean = False)
api_keys = splitString(self.conf('api_key')) api_keys = splitString(self.conf('api_key'), clean = False)
extra_score = splitString(self.conf('extra_score')) extra_score = splitString(self.conf('extra_score'), clean = False)
list = [] list = []
for nr in range(len(hosts)): for nr in range(len(hosts)):
try: key = api_keys[nr]
except: key = ''
try: host = hosts[nr]
except: host = ''
list.append({ list.append({
'use': uses[nr], 'use': uses[nr],
'host': hosts[nr], 'host': host,
'api_key': api_keys[nr], 'api_key': key,
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0 'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
}) })
@@ -23,7 +23,7 @@ class NzbIndex(NZBProvider, RSS):
def _searchOnTitle(self, title, movie, quality, results): def _searchOnTitle(self, title, movie, quality, results):
q = '"%s %s"' % (title, movie['library']['year']) q = '"%s %s" | "%s (%s)"' % (title, movie['library']['year'], title, movie['library']['year'])
arguments = tryUrlencode({ arguments = tryUrlencode({
'q': q, 'q': q,
'age': Env.setting('retention', 'nzb'), 'age': Env.setting('retention', 'nzb'),
@@ -0,0 +1,59 @@
from .main import AwesomeHD
def start():
return AwesomeHD()
config = [{
'name': 'awesomehd',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'list': 'torrent_providers',
'name': 'Awesome-HD',
'description': 'See <a href="https://awesome-hd.net">AHD</a>',
'wizard': True,
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'passkey',
'default': '',
},
{
'name': 'only_internal',
'advanced': True,
'type': 'bool',
'default': 1,
'description': 'Only search for internal releases.'
},
{
'name': 'prefer_internal',
'advanced': True,
'type': 'bool',
'default': 1,
'description': 'Favors internal releases over non-internal releases.'
},
{
'name': 'favor',
'advanced': True,
'default': 'both',
'type': 'dropdown',
'values': [('Encodes & Remuxes', 'both'), ('Encodes', 'encode'), ('Remuxes', 'remux'), ('None', 'none')],
'description': 'Give extra scoring to encodes or remuxes.'
},
{
'name': 'extra_score',
'advanced': True,
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
},
],
},
],
}]
@@ -0,0 +1,64 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import re
import traceback
log = CPLog(__name__)
class AwesomeHD(TorrentProvider):
urls = {
'test' : 'https://awesome-hd.net/',
'detail' : 'https://awesome-hd.net/torrents.php?torrentid=%s',
'search' : 'https://awesome-hd.net/searchapi.php?action=imdbsearch&passkey=%s&imdb=%s&internal=%s',
'download' : 'https://awesome-hd.net/torrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s',
}
http_time_between_calls = 1
def _search(self, movie, quality, results):
data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), movie['library']['identifier'], self.conf('only_internal')))
if data:
try:
soup = BeautifulSoup(data)
authkey = soup.find('authkey').get_text()
entries = soup.find_all('torrent')
for entry in entries:
torrentscore = 0
torrent_id = entry.find('id').get_text()
name = entry.find('name').get_text()
year = entry.find('year').get_text()
releasegroup = entry.find('releasegroup').get_text()
resolution = entry.find('resolution').get_text()
encoding = entry.find('encoding').get_text()
freeleech = entry.find('freeleech').get_text()
torrent_desc = '/ %s / %s / %s ' % (releasegroup, resolution, encoding)
if freeleech == '0.25' and self.conf('prefer_internal'):
torrent_desc += '/ Internal'
torrentscore += 200
if encoding == 'x264' and self.conf('favor') in ['encode', 'both']:
torrentscore += 300
if re.search('Remux', encoding) and self.conf('favor') in ['remux', 'both']:
torrentscore += 200
results.append({
'id': torrent_id,
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
'url': self.urls['download'] % (torrent_id, authkey, self.conf('passkey')),
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(entry.find('size').get_text()),
'seeders': tryInt(entry.find('seeders').get_text()),
'leechers': tryInt(entry.find('leechers').get_text()),
'score': torrentscore
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
@@ -0,0 +1,45 @@
from .main import HDBits
def start():
return HDBits()
config = [{
'name': 'hdbits',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'list': 'torrent_providers',
'name': 'HDBits',
'description': 'See <a href="http://hdbits.org">HDBits</a>',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'passkey',
'default': '',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
},
],
},
],
}]
@@ -0,0 +1,58 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import traceback
log = CPLog(__name__)
class HDBits(TorrentProvider):
urls = {
'test' : 'https://hdbits.org/',
'login' : 'https://hdbits.org/login/doLogin/',
'detail' : 'https://hdbits.org/details.php?id=%s&source=browse',
'search' : 'https://hdbits.org/json_search.php?imdb=%s',
'download' : 'https://hdbits.org/download.php/%s.torrent?id=%s&passkey=%s&source=details.browse',
'login_check': 'http://hdbits.org/inbox.php',
}
http_time_between_calls = 1 #seconds
def _search(self, movie, quality, results):
data = self.getJsonData(self.urls['search'] % movie['library']['identifier'], opener = self.login_opener)
if data:
try:
for result in data:
results.append({
'id': result['id'],
'name': result['title'],
'url': self.urls['download'] % (result['id'], result['id'], self.conf('passkey')),
'detail_url': self.urls['detail'] % result['id'],
'size': self.parseSize(result['size']),
'seeders': tryInt(result['seeder']),
'leechers': tryInt(result['leecher'])
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
data = self.getHTMLData('https://hdbits.org/login')
bs = BeautifulSoup(data)
secret = bs.find('input', attrs = {'name': 'lol'})['value']
return tryUrlencode({
'uname': self.conf('username'),
'password': self.conf('password'),
'lol': secret
})
def loginSuccess(self, output):
return '/logout.php' in output.lower()
loginCheckSuccess = loginSuccess
@@ -5,7 +5,6 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider from couchpotato.core.providers.torrent.base import TorrentProvider
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -15,7 +14,8 @@ class IPTorrents(TorrentProvider):
'test' : 'http://www.iptorrents.com/', 'test' : 'http://www.iptorrents.com/',
'base_url' : 'http://www.iptorrents.com', 'base_url' : 'http://www.iptorrents.com',
'login' : 'http://www.iptorrents.com/torrents/', 'login' : 'http://www.iptorrents.com/torrents/',
'search' : 'http://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti', 'login_check': 'http://www.iptorrents.com/inbox.php',
'search' : 'http://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti&p=%d',
} }
cat_ids = [ cat_ids = [
@@ -32,48 +32,62 @@ class IPTorrents(TorrentProvider):
freeleech = '' if not self.conf('freeleech') else '&free=on' freeleech = '' if not self.conf('freeleech') else '&free=on'
url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year']))) pages = 1
data = self.getHTMLData(url, opener = self.login_opener) current_page = 1
while current_page <= pages and not self.shuttingDown():
if data: url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), current_page)
html = BeautifulSoup(data) data = self.getHTMLData(url, opener = self.login_opener)
try: if data:
result_table = html.find('table', attrs = {'class' : 'torrents'}) html = BeautifulSoup(data)
if not result_table or 'nothing found!' in data.lower(): try:
return page_nav = html.find('span', attrs = {'class' : 'page_nav'})
if page_nav:
next_link = page_nav.find("a", text = "Next")
if next_link:
final_page_link = next_link.previous_sibling.previous_sibling
pages = int(final_page_link.string)
entries = result_table.find_all('tr') result_table = html.find('table', attrs = {'class' : 'torrents'})
for result in entries[1:]: if not result_table or 'nothing found!' in data.lower():
return
torrent = result.find_all('td')[1].find('a') entries = result_table.find_all('tr')
torrent_id = torrent['href'].replace('/details.php?id=', '') for result in entries[1:]:
torrent_name = torrent.string
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
torrent_details_url = self.urls['base_url'] + torrent['href']
torrent_size = self.parseSize(result.find_all('td')[5].string)
torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string)
torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string)
results.append({ torrent = result.find_all('td')
'id': torrent_id, if len(torrent) <= 1:
'name': torrent_name, break
'url': torrent_download_url,
'detail_url': torrent_details_url,
'download': self.loginDownload,
'size': torrent_size,
'seeders': torrent_seeders,
'leechers': torrent_leechers,
})
except: torrent = torrent[1].find('a')
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
def loginSuccess(self, output): torrent_id = torrent['href'].replace('/details.php?id=', '')
return 'don\'t have an account' not in output.lower() torrent_name = torrent.string
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
torrent_details_url = self.urls['base_url'] + torrent['href']
torrent_size = self.parseSize(result.find_all('td')[5].string)
torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string)
torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string)
results.append({
'id': torrent_id,
'name': torrent_name,
'url': torrent_download_url,
'detail_url': torrent_details_url,
'size': torrent_size,
'seeders': torrent_seeders,
'leechers': torrent_leechers,
})
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
break
current_page += 1
def getLoginParams(self): def getLoginParams(self):
return tryUrlencode({ return tryUrlencode({
@@ -81,3 +95,9 @@ class IPTorrents(TorrentProvider):
'password': self.conf('password'), 'password': self.conf('password'),
'login': 'submit', 'login': 'submit',
}) })
def loginSuccess(self, output):
return 'don\'t have an account' not in output.lower()
def loginCheckSuccess(self, output):
return '/logout.php' in output.lower()
@@ -11,9 +11,9 @@ log = CPLog(__name__)
class KickAssTorrents(TorrentMagnetProvider): class KickAssTorrents(TorrentMagnetProvider):
urls = { urls = {
'test': 'https://kat.ph/', 'test': 'https://kickass.to/',
'detail': 'https://kat.ph/%s', 'detail': 'https://kickass.to/%s',
'search': 'https://kat.ph/%s-i%s/', 'search': 'https://kickass.to/%s-i%s/',
} }
cat_ids = [ cat_ids = [
@@ -38,6 +38,30 @@ config = [{
'name': 'passkey', 'name': 'passkey',
'default': '', 'default': '',
}, },
{
'name': 'prefer_golden',
'advanced': True,
'type': 'bool',
'label': 'Prefer golden',
'default': 1,
'description': 'Favors Golden Popcorn-releases over all other releases.'
},
{
'name': 'prefer_scene',
'advanced': True,
'type': 'bool',
'label': 'Prefer scene',
'default': 0,
'description': 'Favors scene-releases over non-scene releases.'
},
{
'name': 'require_approval',
'advanced': True,
'type': 'bool',
'label': 'Require approval',
'default': 0,
'description': 'Require staff-approval for releases to be accepted.'
},
{ {
'name': 'extra_score', 'name': 'extra_score',
'advanced': True, 'advanced': True,
@@ -3,13 +3,11 @@ from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider from couchpotato.core.providers.torrent.base import TorrentProvider
from dateutil.parser import parse from dateutil.parser import parse
import cookielib
import htmlentitydefs import htmlentitydefs
import json import json
import re import re
import time import time
import traceback import traceback
import urllib2
log = CPLog(__name__) log = CPLog(__name__)
@@ -21,9 +19,12 @@ class PassThePopcorn(TorrentProvider):
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s', 'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s',
'torrent': 'https://tls.passthepopcorn.me/torrents.php', 'torrent': 'https://tls.passthepopcorn.me/torrents.php',
'login': 'https://tls.passthepopcorn.me/ajax.php?action=login', 'login': 'https://tls.passthepopcorn.me/ajax.php?action=login',
'login_check': 'https://tls.passthepopcorn.me/ajax.php?action=login',
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d' 'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d'
} }
http_time_between_calls = 2
quality_search_params = { quality_search_params = {
'bd50': {'media': 'Blu-ray', 'format': 'BD50'}, 'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
'1080p': {'resolution': '1080p'}, '1080p': {'resolution': '1080p'},
@@ -52,18 +53,6 @@ class PassThePopcorn(TorrentProvider):
'cam': {'Source': ['CAM']} 'cam': {'Source': ['CAM']}
} }
class NotLoggedInHTTPError(urllib2.HTTPError):
def __init__(self, url, code, msg, headers, fp):
urllib2.HTTPError.__init__(self, url, code, msg, headers, fp)
class PTPHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
def http_error_302(self, req, fp, code, msg, headers):
log.debug("302 detected; redirected to %s" % headers['Location'])
if (headers['Location'] != 'login.php'):
return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
else:
raise PassThePopcorn.NotLoggedInHTTPError(req.get_full_url(), code, msg, headers, fp)
def _search(self, movie, quality, results): def _search(self, movie, quality, results):
movie_title = getTitle(movie['library']) movie_title = getTitle(movie['library'])
@@ -75,17 +64,8 @@ class PassThePopcorn(TorrentProvider):
'searchstr': movie['library']['identifier'] 'searchstr': movie['library']['identifier']
}) })
# Do login for the cookies url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
if not self.login_opener and not self.login(): res = self.getJsonData(url, opener = self.login_opener)
return
try:
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
txt = self.urlopen(url, opener = self.login_opener)
res = json.loads(txt)
except:
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)' % params)
return
try: try:
if not 'Movies' in res: if not 'Movies' in res:
@@ -96,18 +76,23 @@ class PassThePopcorn(TorrentProvider):
for ptpmovie in res['Movies']: for ptpmovie in res['Movies']:
if not 'Torrents' in ptpmovie: if not 'Torrents' in ptpmovie:
log.debug('Movie %s (%s) has NO torrents' % (ptpmovie['Title'], ptpmovie['Year'])) log.debug('Movie %s (%s) has NO torrents', (ptpmovie['Title'], ptpmovie['Year']))
continue continue
log.debug('Movie %s (%s) has %d torrents' % (ptpmovie['Title'], ptpmovie['Year'], len(ptpmovie['Torrents']))) log.debug('Movie %s (%s) has %d torrents', (ptpmovie['Title'], ptpmovie['Year'], len(ptpmovie['Torrents'])))
for torrent in ptpmovie['Torrents']: for torrent in ptpmovie['Torrents']:
torrent_id = tryInt(torrent['Id']) torrent_id = tryInt(torrent['Id'])
torrentdesc = '%s %s %s' % (torrent['Resolution'], torrent['Source'], torrent['Codec']) torrentdesc = '%s %s %s' % (torrent['Resolution'], torrent['Source'], torrent['Codec'])
torrentscore = 0
if 'GoldenPopcorn' in torrent and torrent['GoldenPopcorn']: if 'GoldenPopcorn' in torrent and torrent['GoldenPopcorn']:
torrentdesc += ' HQ' torrentdesc += ' HQ'
if self.conf('prefer_golden'):
torrentscore += 200
if 'Scene' in torrent and torrent['Scene']: if 'Scene' in torrent and torrent['Scene']:
torrentdesc += ' Scene' torrentdesc += ' Scene'
if self.conf('prefer_scene'):
torrentscore += 50
if 'RemasterTitle' in torrent and torrent['RemasterTitle']: if 'RemasterTitle' in torrent and torrent['RemasterTitle']:
torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle']) torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle'])
@@ -115,64 +100,44 @@ class PassThePopcorn(TorrentProvider):
torrent_name = re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) - %s' % (movie_title, ptpmovie['Year'], torrentdesc)) torrent_name = re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) - %s' % (movie_title, ptpmovie['Year'], torrentdesc))
def extra_check(item): def extra_check(item):
return self.torrentMeetsQualitySpec(item, type) return self.torrentMeetsQualitySpec(item, quality_id)
results.append({ results.append({
'id': torrent_id, 'id': torrent_id,
'name': torrent_name, 'name': torrent_name,
'Source': torrent['Source'],
'Checked': 'true' if torrent['Checked'] else 'false',
'Resolution': torrent['Resolution'],
'url': '%s?action=download&id=%d&authkey=%s&torrent_pass=%s' % (self.urls['torrent'], torrent_id, authkey, passkey), 'url': '%s?action=download&id=%d&authkey=%s&torrent_pass=%s' % (self.urls['torrent'], torrent_id, authkey, passkey),
'detail_url': self.urls['detail'] % torrent_id, 'detail_url': self.urls['detail'] % torrent_id,
'date': tryInt(time.mktime(parse(torrent['UploadTime']).timetuple())), 'date': tryInt(time.mktime(parse(torrent['UploadTime']).timetuple())),
'size': tryInt(torrent['Size']) / (1024 * 1024), 'size': tryInt(torrent['Size']) / (1024 * 1024),
'seeders': tryInt(torrent['Seeders']), 'seeders': tryInt(torrent['Seeders']),
'leechers': tryInt(torrent['Leechers']), 'leechers': tryInt(torrent['Leechers']),
'score': 50 if torrent['GoldenPopcorn'] else 0, 'score': torrentscore,
'extra_check': extra_check, 'extra_check': extra_check,
'download': self.loginDownload,
}) })
except: except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def login(self):
cookieprocessor = urllib2.HTTPCookieProcessor(cookielib.CookieJar())
opener = urllib2.build_opener(cookieprocessor, PassThePopcorn.PTPHTTPRedirectHandler())
opener.addheaders = [
('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.75 Safari/537.1'),
('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
('Accept-Language', 'en-gb,en;q=0.5'),
('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'),
('Keep-Alive', '115'),
('Connection', 'keep-alive'),
('Cache-Control', 'max-age=0'),
]
try:
response = opener.open(self.urls['login'], self.getLoginParams())
except urllib2.URLError as e:
log.error('Login to PassThePopcorn failed: %s' % e)
return False
if response.getcode() == 200:
log.debug('Login HTTP status 200; seems successful')
self.login_opener = opener
return True
else:
log.error('Login to PassThePopcorn failed: returned code %d' % response.getcode())
return False
def torrentMeetsQualitySpec(self, torrent, quality): def torrentMeetsQualitySpec(self, torrent, quality):
if not quality in self.post_search_filters: if not quality in self.post_search_filters:
return True return True
for field, specs in self.post_search_filters[quality].items(): reqs = self.post_search_filters[quality].copy()
if self.conf('require_approval'):
log.debug('Config: Require staff-approval activated')
reqs['Checked'] = ['true']
for field, specs in reqs.items():
matches_one = False matches_one = False
seen_one = False seen_one = False
if not field in torrent: if not field in torrent:
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"' % (torrent['Id'], field, quality)) log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"', (torrent['id'], field, quality))
continue continue
for spec in specs: for spec in specs:
@@ -182,11 +147,14 @@ class PassThePopcorn(TorrentProvider):
return False return False
else: else:
# a positive rule; if any of the possible positive values match the field, return True # a positive rule; if any of the possible positive values match the field, return True
log.debug('Checking if torrents field %s equals %s' % (field, spec))
seen_one = True seen_one = True
if torrent[field] == spec: if torrent[field] == spec:
log.debug('Torrent satisfied %s == %s' % (field, spec))
matches_one = True matches_one = True
if seen_one and not matches_one: if seen_one and not matches_one:
log.debug('Torrent did not satisfy requirements, ignoring')
return False return False
return True return True
@@ -227,3 +195,11 @@ class PassThePopcorn(TorrentProvider):
'keeplogged': '1', 'keeplogged': '1',
'login': 'Login' 'login': 'Login'
}) })
def loginSuccess(self, output):
try:
return json.loads(output).get('Result', '').lower() == 'ok'
except:
return False
loginCheckSuccess = loginSuccess
@@ -12,7 +12,8 @@ class SceneAccess(TorrentProvider):
urls = { urls = {
'test': 'https://www.sceneaccess.eu/', 'test': 'https://www.sceneaccess.eu/',
'login' : 'https://www.sceneaccess.eu/login', 'login': 'https://www.sceneaccess.eu/login',
'login_check': 'https://www.sceneaccess.eu/inbox',
'detail': 'https://www.sceneaccess.eu/details?id=%s', 'detail': 'https://www.sceneaccess.eu/details?id=%s',
'search': 'https://www.sceneaccess.eu/browse?method=2&c%d=%d', 'search': 'https://www.sceneaccess.eu/browse?method=2&c%d=%d',
'download': 'https://www.sceneaccess.eu/%s', 'download': 'https://www.sceneaccess.eu/%s',
@@ -39,9 +40,6 @@ class SceneAccess(TorrentProvider):
}) })
url = "%s&%s" % (url, arguments) url = "%s&%s" % (url, arguments)
# Do login for the cookies
if not self.login_opener and not self.login():
return
data = self.getHTMLData(url, opener = self.login_opener) data = self.getHTMLData(url, opener = self.login_opener)
@@ -69,7 +67,6 @@ class SceneAccess(TorrentProvider):
'size': self.parseSize(result.find('td', attrs = {'class' : 'ttr_size'}).contents[0]), 'size': self.parseSize(result.find('td', attrs = {'class' : 'ttr_size'}).contents[0]),
'seeders': tryInt(result.find('td', attrs = {'class' : 'ttr_seeders'}).find('a').string), 'seeders': tryInt(result.find('td', attrs = {'class' : 'ttr_seeders'}).find('a').string),
'leechers': tryInt(leechers.string) if leechers else 0, 'leechers': tryInt(leechers.string) if leechers else 0,
'download': self.loginDownload,
'get_more_info': self.getMoreInfo, 'get_more_info': self.getMoreInfo,
}) })
@@ -91,3 +88,8 @@ class SceneAccess(TorrentProvider):
item['description'] = description item['description'] = description
return item return item
def loginSuccess(self, output):
return '/inbox' in output.lower()
loginCheckSuccess = loginSuccess
@@ -13,6 +13,7 @@ class SceneHD(TorrentProvider):
urls = { urls = {
'test': 'https://scenehd.org/', 'test': 'https://scenehd.org/',
'login' : 'https://scenehd.org/takelogin.php', 'login' : 'https://scenehd.org/takelogin.php',
'login_check': 'https://scenehd.org/my.php',
'detail': 'https://scenehd.org/details.php?id=%s', 'detail': 'https://scenehd.org/details.php?id=%s',
'search': 'https://scenehd.org/browse.php?ajax', 'search': 'https://scenehd.org/browse.php?ajax',
'download': 'https://scenehd.org/download.php?id=%s', 'download': 'https://scenehd.org/download.php?id=%s',
@@ -28,10 +29,6 @@ class SceneHD(TorrentProvider):
}) })
url = "%s&%s" % (self.urls['search'], arguments) url = "%s&%s" % (self.urls['search'], arguments)
# Cookie login
if not self.login_opener and not self.login():
return
data = self.getHTMLData(url, opener = self.login_opener) data = self.getHTMLData(url, opener = self.login_opener)
if data: if data:
@@ -61,7 +58,6 @@ class SceneHD(TorrentProvider):
'seeders': tryInt(all_cells[10].find('a').string), 'seeders': tryInt(all_cells[10].find('a').string),
'leechers': tryInt(leechers), 'leechers': tryInt(leechers),
'url': self.urls['download'] % torrent_id, 'url': self.urls['download'] % torrent_id,
'download': self.loginDownload,
'description': all_cells[1].find('a')['href'], 'description': all_cells[1].find('a')['href'],
}) })
@@ -75,3 +71,9 @@ class SceneHD(TorrentProvider):
'password': self.conf('password'), 'password': self.conf('password'),
'ssl': 'yes', 'ssl': 'yes',
}) })
def loginSuccess(self, output):
return 'logout.php' in output.lower()
loginCheckSuccess = loginSuccess
@@ -35,8 +35,11 @@ class ThePirateBay(TorrentMagnetProvider):
'https://piratereverse.info', 'https://piratereverse.info',
'https://tpb.pirateparty.org.uk', 'https://tpb.pirateparty.org.uk',
'https://argumentomteemigreren.nl', 'https://argumentomteemigreren.nl',
'https://livepirate.com/', 'https://livepirate.com',
'https://www.getpirate.com/', 'https://www.getpirate.com',
'https://tpb.partipirate.org',
'https://tpb.piraten.lu',
'https://kuiken.co',
] ]
def __init__(self): def __init__(self):
@@ -50,7 +53,7 @@ class ThePirateBay(TorrentMagnetProvider):
while page < total_pages: while page < total_pages:
search_url = self.urls['search'] % (self.getDomain(), tryUrlencode('"%s %s"' % (title, movie['library']['year'])), page, self.getCatId(quality['identifier'])[0]) search_url = self.urls['search'] % (self.getDomain(), tryUrlencode('"%s" %s' % (title, movie['library']['year'])), page, self.getCatId(quality['identifier'])[0])
page += 1 page += 1
data = self.getHTMLData(search_url) data = self.getHTMLData(search_url)
@@ -0,0 +1,42 @@
from .main import TorrentBytes
def start():
return TorrentBytes()
config = [{
'name': 'torrentbytes',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'list': 'torrent_providers',
'name': 'TorrentBytes',
'description': 'See <a href="http://torrentbytes.net">TorrentBytes</a>',
'wizard': True,
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]
@@ -0,0 +1,82 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import traceback
log = CPLog(__name__)
class TorrentBytes(TorrentProvider):
urls = {
'test' : 'https://www.torrentbytes.net/',
'login' : 'https://www.torrentbytes.net/takelogin.php',
'login_check' : 'https://www.torrentbytes.net/inbox.php',
'detail' : 'https://www.torrentbytes.net/details.php?id=%s',
'search' : 'https://www.torrentbytes.net/browse.php?search=%s&cat=%d',
'download' : 'https://www.torrentbytes.net/download.php?id=%s&name=%s',
}
cat_ids = [
([5], ['720p', '1080p']),
([19], ['cam']),
([19], ['ts', 'tc']),
([19], ['r5', 'scr']),
([19], ['dvdrip']),
([5], ['brrip']),
([20], ['dvdr']),
]
http_time_between_calls = 1 #seconds
cat_backup_id = None
def _searchOnTitle(self, title, movie, quality, results):
url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0])
data = self.getHTMLData(url, opener = self.login_opener)
if data:
html = BeautifulSoup(data)
try:
result_table = html.find('table', attrs = {'border' : '1'})
if not result_table:
return
entries = result_table.find_all('tr')
for result in entries[1:]:
cells = result.find_all('td')
link = cells[1].find('a', attrs = {'class' : 'index'})
full_id = link['href'].replace('details.php?id=', '')
torrent_id = full_id[:6]
results.append({
'id': torrent_id,
'name': link.contents[0],
'url': self.urls['download'] % (torrent_id, link.contents[0]),
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]),
'seeders': tryInt(cells[8].find('span').contents[0]),
'leechers': tryInt(cells[9].find('span').contents[0]),
})
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return tryUrlencode({
'username': self.conf('username'),
'password': self.conf('password'),
'login': 'submit',
})
def loginSuccess(self, output):
return 'logout.php' in output.lower() or 'Welcome' in output.lower()
loginCheckSuccess = loginSuccess
@@ -10,7 +10,8 @@ class TorrentDay(TorrentProvider):
urls = { urls = {
'test': 'http://www.td.af/', 'test': 'http://www.td.af/',
'login' : 'http://www.td.af/torrents/', 'login': 'http://www.td.af/torrents/',
'login_check': 'http://www.torrentday.com/userdetails.php',
'detail': 'http://www.td.af/details.php?id=%s', 'detail': 'http://www.td.af/details.php?id=%s',
'search': 'http://www.td.af/V3/API/API.php', 'search': 'http://www.td.af/V3/API/API.php',
'download': 'http://www.td.af/download.php/%s/%s', 'download': 'http://www.td.af/download.php/%s/%s',
@@ -50,7 +51,6 @@ class TorrentDay(TorrentProvider):
'size': self.parseSize(torrent.get('size')), 'size': self.parseSize(torrent.get('size')),
'seeders': tryInt(torrent.get('seed')), 'seeders': tryInt(torrent.get('seed')),
'leechers': tryInt(torrent.get('leech')), 'leechers': tryInt(torrent.get('leech')),
'download': self.loginDownload,
}) })
def getLoginParams(self): def getLoginParams(self):
@@ -62,3 +62,6 @@ class TorrentDay(TorrentProvider):
def loginSuccess(self, output): def loginSuccess(self, output):
return 'Password not correct' not in output return 'Password not correct' not in output
def loginCheckSuccess(self, output):
return 'logout.php' in output.lower()
@@ -14,6 +14,7 @@ class TorrentLeech(TorrentProvider):
urls = { urls = {
'test' : 'http://www.torrentleech.org/', 'test' : 'http://www.torrentleech.org/',
'login' : 'http://www.torrentleech.org/user/account/login/', 'login' : 'http://www.torrentleech.org/user/account/login/',
'login_check': 'http://torrentleech.org/user/messages',
'detail' : 'http://www.torrentleech.org/torrent/%s', 'detail' : 'http://www.torrentleech.org/torrent/%s',
'search' : 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d', 'search' : 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
'download' : 'http://www.torrentleech.org%s', 'download' : 'http://www.torrentleech.org%s',
@@ -58,7 +59,6 @@ class TorrentLeech(TorrentProvider):
'name': link.string, 'name': link.string,
'url': self.urls['download'] % url['href'], 'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % details['href'], 'detail_url': self.urls['download'] % details['href'],
'download': self.loginDownload,
'size': self.parseSize(result.find_all('td')[4].string), 'size': self.parseSize(result.find_all('td')[4].string),
'seeders': tryInt(result.find('td', attrs = {'class' : 'seeders'}).string), 'seeders': tryInt(result.find('td', attrs = {'class' : 'seeders'}).string),
'leechers': tryInt(result.find('td', attrs = {'class' : 'leechers'}).string), 'leechers': tryInt(result.find('td', attrs = {'class' : 'leechers'}).string),
@@ -77,3 +77,5 @@ class TorrentLeech(TorrentProvider):
def loginSuccess(self, output): def loginSuccess(self, output):
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower() return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
loginCheckSuccess = loginSuccess
@@ -0,0 +1,47 @@
from .main import TorrentShack
def start():
return TorrentShack()
config = [{
'name': 'torrentshack',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'list': 'torrent_providers',
'name': 'TorrentShack',
'description': 'See <a href="http://www.torrentshack.net/">TorrentShack</a>',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'scene_only',
'type': 'bool',
'default': False,
'description': 'Only allow scene releases.'
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]
@@ -0,0 +1,83 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import traceback
log = CPLog(__name__)
class TorrentShack(TorrentProvider):
urls = {
'test' : 'http://www.torrentshack.net/',
'login' : 'http://www.torrentshack.net/login.php',
'login_check': 'http://www.torrentshack.net/inbox.php',
'detail' : 'http://www.torrentshack.net/torrent/%s',
'search' : 'http://www.torrentshack.net/torrents.php?searchstr=%s&filter_cat[%d]=1',
'download' : 'http://www.torrentshack.net/%s',
}
cat_ids = [
([970], ['bd50']),
([300], ['720p', '1080p']),
([350], ['dvdr']),
([400], ['brrip', 'dvdrip']),
]
http_time_between_calls = 1 #seconds
cat_backup_id = None
def _searchOnTitle(self, title, movie, quality, results):
url = self.urls['search'] % (tryUrlencode('"%s" %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0])
data = self.getHTMLData(url, opener = self.login_opener)
if data:
html = BeautifulSoup(data)
try:
result_table = html.find('table', attrs = {'id' : 'torrent_table'})
if not result_table:
return
entries = result_table.find_all('tr', attrs = {'class' : 'torrent'})
for result in entries:
link = result.find('span', attrs = {'class' : 'torrent_name_link'}).parent
url = result.find('td', attrs = {'class' : 'torrent_td'}).find('a')
extra_info = ''
if result.find('span', attrs = {'class' : 'torrent_extra_info'}):
extra_info = result.find('span', attrs = {'class' : 'torrent_extra_info'}).text
if not self.conf('scene_only') or extra_info != '[NotScene]':
results.append({
'id': link['href'].replace('torrents.php?torrentid=', ''),
'name': unicode(link.span.string).translate({ord(u'\xad'): None}),
'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % link['href'],
'size': self.parseSize(result.find_all('td')[4].string),
'seeders': tryInt(result.find_all('td')[6].string),
'leechers': tryInt(result.find_all('td')[7].string),
})
else:
log.info('Not adding release %s [NotScene]' % unicode(link.span.string).translate({ord(u'\xad'): None}))
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return tryUrlencode({
'username': self.conf('username'),
'password': self.conf('password'),
'keeplogged': '1',
'login': 'Login',
})
def loginSuccess(self, output):
return 'logout.php' in output.lower()
loginCheckSuccess = loginSuccess
@@ -0,0 +1,33 @@
from main import Yify
def start():
return Yify()
config = [{
'name': 'yify',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'list': 'torrent_providers',
'name': 'Yify',
'description': 'Free provider, less accurate. Small HD movies, encoded by <a href="https://yify-torrents.com/">Yify</a>.',
'wizard': False,
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': 0
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
}
]
}]

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