Compare commits

..
Author SHA1 Message Date
Ruud 9a314cfbc4 One up 2012-12-29 00:03:45 +01:00
Ruud 5941d0bf77 Add version to update url 2012-12-29 00:03:36 +01:00
Ruud d326c1c25c Merge branch 'refs/heads/master' into desktop
Conflicts:
	version.py
2012-12-28 23:31:08 +01:00
Ruud 7e6234298d Merge branch 'refs/heads/develop' 2012-12-28 23:25:40 +01:00
Ruud 5cf4b8b4d3 Binsearch provider 2012-12-28 23:01:35 +01:00
Ruud 6e56072250 Don't migrate when in development 2012-12-28 17:44:02 +01:00
Ruud 917c5552a4 Simplified providers 2012-12-27 19:53:12 +01:00
Ruud 73c5b90232 TorrentDay support. closes #1161 2012-12-25 18:38:37 +01:00
Ruud fd53ba0637 SceneAccess: Don't add quality to query 2012-12-25 17:43:47 +01:00
Ruud 0ef3906b3d Cleanup 2012-12-25 17:15:09 +01:00
Ruud 5ab0d7a97b Cleanup torrent providers 2012-12-25 14:56:27 +01:00
Ruud dbbbbb2f84 Module update: dateutil 2012-12-25 11:38:49 +01:00
Ruud 1bfe948a45 Newznab didn't return results 2012-12-24 19:21:43 +01:00
Ruud 0d2dcff7f0 NZB Provider cleanup 2012-12-23 02:48:36 +01:00
Ruud d4da206f93 Merge branch 'refs/heads/develop' 2012-12-22 16:33:47 +01:00
Ruud 439cda8b63 Newznab age wrong. fix #1171 2012-12-22 16:33:28 +01:00
Ruud bbe8362b08 Show updating screen instantly. closes #1167 2012-12-21 23:54:41 +01:00
Ruud 985a168724 Merge branch 'refs/heads/develop' 2012-12-21 23:18:00 +01:00
Ruud 5e6aea97f7 Score providers 2012-12-21 23:17:50 +01:00
Ruud 6c7c4c7aba Use same api call for all qualities. closes #1164 2012-12-21 23:17:42 +01:00
Ruud e2f59f5ff4 Merge branch 'develop' of github.com:RuudBurger/CouchPotatoServer into develop 2012-12-21 22:15:18 +01:00
Ruud b225980ce7 Use pubDate and enclosure length for newznab 2012-12-21 22:14:37 +01:00
Ruud b8e86b378f NZBx provider 2012-12-20 15:45:04 +01:00
Ruud 031a186d71 NZBx fixes 2012-12-20 15:19:40 +01:00
Ruud 3c04eed218 Added nzbx option 2012-12-20 15:18:46 +01:00
Ruud 17e01689d9 Remove torrage.ws. fix #1157 2012-12-19 16:13:40 +01:00
Ruud 173c6194ed Merge branch 'refs/heads/develop' 2012-12-19 11:12:26 +01:00
Ruud 95c2e992b0 Use trailer naming from settings. closes #936 2012-12-19 11:11:12 +01:00
Ruud 4bffb299af Catch urlerrors. closes #1154 2012-12-19 08:01:35 +01:00
Ruud a2c4119508 Change PublicHD to .se TLD 2012-12-18 23:36:13 +01:00
Ruud 4e9472f8ee Encode path properly before using it in walk. close #978 2012-12-18 23:18:54 +01:00
Ruud f7911fe9f3 Remove release on new scan 2012-12-18 14:14:06 +01:00
Ruud 8ffa6a8392 Quality by id 2012-12-18 13:54:13 +01:00
Ruud 382d49f895 Delete release if it has no files 2012-12-17 22:41:56 +01:00
Ruud 570b79a67e Use height with margin to check quality. fix #582 2012-12-17 22:27:07 +01:00
Ruud e7aafc406f Check if identifier exists before adding release. fix #1048 2012-12-17 21:10:30 +01:00
Ruud 2dcc1e096e Make path safe first 2012-12-17 21:10:04 +01:00
Ruud 9f0746a668 Encoding issues. fix #974 2012-12-17 20:50:58 +01:00
Ruud d9c437bd7f Fix some torrentleech stuff. closes #1149 2012-12-17 19:55:27 +01:00
Ruud 7079647f87 Also try and find movie name between [] 2012-12-17 18:49:37 +01:00
Ruud 65570ba479 Improve name searching. closes #1137 2012-12-17 18:22:12 +01:00
Ruud a57ba9026d Year match only 1900-2099 2012-12-17 18:21:15 +01:00
Ruud 63246256ee Don't remove stuff from python cache 2012-12-17 17:10:53 +01:00
Ruud 1ac0dc3bbf Don't show Environment vars when developing 2012-12-17 16:40:15 +01:00
Ruud bcd23ad10c Merge branch 'refs/heads/develop' 2012-12-17 15:13:00 +01:00
Ruud 342d31b48a Remove ignored words which are part of title. close #1123 2012-12-17 14:08:44 +01:00
Ruud ea7904ed9a Typo on seeders check. fix #1142 2012-12-17 13:56:11 +01:00
Ruud ca37c2f018 Merge branch 'develop-renamer' of https://github.com/clinton-hall/CouchPotatoServer into develop 2012-12-17 13:18:23 +01:00
Ruud 5aa2146614 OMGWTFNZBs support. closes #1130
ZOMG BBQ SAUCAGES NOMNOMNOM
2012-12-17 13:07:01 +01:00
Ruud 0fd49a2c67 FTDWorld returned wrong backup category 2012-12-17 13:03:33 +01:00
Ruud b680d84cba Don't use handler when in desktop build 2012-12-17 12:00:42 +01:00
Ruud 898e6f487d Merge branch 'refs/heads/develop' 2012-12-16 23:52:06 +01:00
clinton-hall bb7b4cbbed Added try: except for two common errors
Does not fix the errors, but prevents the renamer being stuck as "in progress"
Allows next instance to run.
2012-12-13 19:45:13 -08:00
Ruud 6618c3927c Merge branch 'refs/heads/develop' 2012-12-11 23:15:06 +01:00
Ruud 4b58b40226 Merge branch 'refs/heads/develop' 2012-12-01 11:48:54 +01:00
Ruud 3ecc826629 Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2012-11-11 22:06:48 +01:00
Ruud 32fe3796e4 Merge branch 'refs/heads/develop' 2012-10-26 22:22:47 +02:00
Ruud 359d1aaafa Merge branch 'refs/heads/develop' 2012-10-26 14:54:12 +02:00
Ruud fb5d336351 Merge branch 'refs/heads/develop' 2012-10-26 14:36:04 +02:00
Ruud eb30dff986 Merge branch 'refs/heads/develop' 2012-10-13 00:00:44 +02:00
Ruud 9312336962 Merge branch 'refs/heads/develop' 2012-09-24 09:36:59 +02:00
Ruud ade4338ea6 Merge branch 'refs/heads/develop' 2012-09-16 21:32:16 +02:00
Ruud 55b20324c0 Merge branch 'refs/heads/develop' 2012-09-16 12:36:48 +02:00
Ruud c0fb28301d Merge branch 'refs/heads/develop'
Conflicts:
	version.py
2012-09-16 10:46:39 +02:00
Ruud f9c2503f81 Merge branch 'refs/heads/develop' 2012-09-14 13:15:35 +02:00
Ruud 5b4cdf05b1 Merge branch 'refs/heads/develop' 2012-09-14 13:06:56 +02:00
Ruud 6f25a6bdfd Merge branch 'refs/heads/develop' 2012-09-03 10:32:09 +02:00
Ruud 23427e95f7 Merge branch 'refs/heads/develop' 2012-08-26 23:09:51 +02:00
Ruud 90a09e573b Merge branch 'refs/heads/develop'
Conflicts:
	couchpotato/core/_base/updater/main.py
2012-08-05 16:15:53 +02:00
Ruud e1d7440b9d Wrong branch in master 2012-07-15 00:23:44 +02:00
59 changed files with 1620 additions and 1043 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
from esky.util import appdir_from_executable #@UnresolvedImport from esky.util import appdir_from_executable #@UnresolvedImport
from threading import Thread from threading import Thread
from version import VERSION
from wx.lib.softwareupdate import SoftwareUpdate from wx.lib.softwareupdate import SoftwareUpdate
import os import os
import sys import sys
@@ -165,7 +166,7 @@ class CouchPotatoApp(wx.App, SoftwareUpdate):
def OnInit(self): def OnInit(self):
# Updater # Updater
base_url = 'http://couchpota.to/updates/' base_url = 'http://couchpota.to/updates/%s/' % VERSION
self.InitUpdates(base_url, base_url + 'changelog.html', self.InitUpdates(base_url, base_url + 'changelog.html',
icon = wx.Icon('icon.png')) icon = wx.Icon('icon.png'))
+2 -1
View File
@@ -53,7 +53,8 @@ class Core(Plugin):
addEvent('setting.save.core.api_key', self.checkApikey) addEvent('setting.save.core.api_key', self.checkApikey)
# Make sure we can close-down with ctrl+c properly # Make sure we can close-down with ctrl+c properly
self.signalHandler() if not Env.get('desktop'):
self.signalHandler()
def md5Password(self, value): def md5Password(self, value):
return md5(value.encode(Env.get('encoding'))) if value else '' return md5(value.encode(Env.get('encoding'))) if value else ''
@@ -90,17 +90,18 @@ var UpdaterBase = new Class({
doUpdate: function(){ doUpdate: function(){
var self = this; var self = this;
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
Api.request('updater.update', { Api.request('updater.update', {
'onComplete': function(json){ 'onComplete': function(json){
if(json.success){ if(json.success)
self.updating(); self.updating();
} else
App.unBlockPage()
} }
}); });
}, },
updating: function(){ updating: function(){
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
App.checkAvailable.delay(500, App, [1000, function(){ App.checkAvailable.delay(500, App, [1000, function(){
window.location.reload(); window.location.reload();
}]); }]);
-1
View File
@@ -14,7 +14,6 @@ class Downloader(Plugin):
torrent_sources = [ torrent_sources = [
'http://torrage.com/torrent/%s.torrent', 'http://torrage.com/torrent/%s.torrent',
'http://torrage.ws/torrent/%s.torrent',
'http://torcache.net/torrent/%s.torrent', 'http://torcache.net/torrent/%s.torrent',
] ]
+3 -3
View File
@@ -12,7 +12,7 @@ def runHandler(name, handler, *args, **kwargs):
return handler(*args, **kwargs) return handler(*args, **kwargs)
except: except:
from couchpotato.environment import Env from couchpotato.environment import Env
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all())) log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all() if not Env.get('dev') else ''))
def addEvent(name, handler, priority = 100): def addEvent(name, handler, priority = 100):
@@ -105,14 +105,14 @@ def fireEvent(name, *args, **kwargs):
# Merge # Merge
if options['merge'] and len(results) > 0: if options['merge'] and len(results) > 0:
# Dict # Dict
if type(results[0]) == dict: if isinstance(results[0], dict):
merged = {} merged = {}
for result in results: for result in results:
merged = mergeDicts(merged, result) merged = mergeDicts(merged, result)
results = merged results = merged
# Lists # Lists
elif type(results[0]) == list: elif isinstance(results[0], list):
merged = [] merged = []
for result in results: for result in results:
merged += result merged += result
+1 -1
View File
@@ -68,7 +68,7 @@ def tryUrlencode(s):
return new[1:] return new[1:]
else: else:
for letter in toUnicode(s): for letter in ss(s):
try: try:
new += quote_plus(letter) new += quote_plus(letter)
except: except:
+11
View File
@@ -1,3 +1,4 @@
from couchpotato.core.helpers.encoding import simplifyString, toSafeString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import hashlib import hashlib
import os.path import os.path
@@ -153,6 +154,16 @@ def getTitle(library_dict):
log.error('Could not get title for library item: %s', library_dict) log.error('Could not get title for library item: %s', library_dict)
return None return None
def possibleTitles(raw_title):
titles = []
titles.append(toSafeString(raw_title).lower())
titles.append(raw_title.lower())
titles.append(simplifyString(raw_title))
return list(set(titles))
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))
+6 -5
View File
@@ -1,9 +1,8 @@
from StringIO import StringIO from StringIO import StringIO
from couchpotato import addView 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, simplifyString, ss, \ from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString
toSafeString from couchpotato.core.helpers.variable import getExt, md5
from couchpotato.core.helpers.variable import getExt
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 flask.templating import render_template_string
@@ -222,7 +221,7 @@ class Plugin(object):
def getCache(self, cache_key, url = None, **kwargs): def getCache(self, cache_key, url = None, **kwargs):
cache_key = simplifyString(cache_key) cache_key = md5(ss(cache_key))
cache = Env.get('cache').get(cache_key) cache = Env.get('cache').get(cache_key)
if cache: if cache:
if not Env.get('dev'): log.debug('Getting cache %s', cache_key) if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
@@ -242,9 +241,11 @@ class Plugin(object):
self.setCache(cache_key, data, timeout = cache_timeout) self.setCache(cache_key, data, timeout = cache_timeout)
return data return data
except: except:
if not kwargs.get('show_error'): if not kwargs.get('show_error', True):
raise raise
return ''
def setCache(self, cache_key, value, timeout = 300): def setCache(self, cache_key, value, timeout = 300):
log.debug('Setting cache %s', cache_key) log.debug('Setting cache %s', cache_key)
Env.get('cache').set(cache_key, value, timeout) Env.get('cache').set(cache_key, value, timeout)
+2
View File
@@ -66,10 +66,12 @@ class FileManager(Plugin):
time.sleep(3) time.sleep(3)
log.debug('Cleaning up unused files') log.debug('Cleaning up unused files')
python_cache = Env.get('cache')._path
try: try:
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: 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:
+30 -7
View File
@@ -115,12 +115,35 @@ class Manage(Plugin):
if done_movie['library']['identifier'] not in added_identifiers: if done_movie['library']['identifier'] not in added_identifiers:
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all') fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
else: else:
for release in done_movie.get('releases', []): for release in done_movie.get('releases', []):
for release_file in release.get('files', []): if len(release.get('files', [])) == 0:
# Remove release not available anymore fireEvent('release.delete', release['id'])
if not os.path.isfile(ss(release_file['path'])): else:
fireEvent('release.clean', release['id']) for release_file in release.get('files', []):
break # Remove release not available anymore
if not os.path.isfile(ss(release_file['path'])):
fireEvent('release.clean', release['id'])
break
# Check if there are duplicate releases (different quality) use the last one, delete the rest
if len(done_movie.get('releases', [])) > 1:
used_files = {}
for release in done_movie.get('releases', []):
for release_file in release.get('files', []):
already_used = used_files.get(release_file['path'])
if already_used:
print already_used, release['id']
if already_used < release['id']:
fireEvent('release.delete', release['id'], single = True) # delete this one
else:
fireEvent('release.delete', already_used, single = True) # delete previous one
break
else:
used_files[release_file['path']] = release.get('id')
del used_files
Env.prop('manage.last_update', time.time()) Env.prop('manage.last_update', time.time())
except: except:
@@ -153,7 +176,7 @@ class Manage(Plugin):
'to_go': total_found, 'to_go': total_found,
} }
if group['library']: if group['library'] and group['library'].get('identifier'):
identifier = group['library'].get('identifier') identifier = group['library'].get('identifier')
added_identifiers.append(identifier) added_identifiers.append(identifier)
@@ -187,5 +210,5 @@ class Manage(Plugin):
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True) groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
for group in groups.itervalues(): for group in groups.itervalues():
if group['library']: if group['library'] and group['library'].get('identifier'):
fireEvent('release.add', group = group) fireEvent('release.add', group = group)
+12 -6
View File
@@ -7,6 +7,7 @@ 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_
import os.path import os.path
import re import re
import time import time
@@ -18,8 +19,8 @@ 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, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']}, {'identifier': '1080p', 'hd': True, 'size': (5000, 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, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']}, {'identifier': '720p', 'hd': True, 'size': (3500, 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'], 'ext':['avi']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], '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')]},
@@ -76,7 +77,7 @@ class QualityPlugin(Plugin):
db = get_session() db = get_session()
quality_dict = {} quality_dict = {}
quality = db.query(Quality).filter_by(identifier = identifier).first() quality = db.query(Quality).filter(or_(Quality.identifier == identifier, Quality.id == identifier)).first()
if quality: if quality:
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict()) quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
@@ -198,9 +199,14 @@ class QualityPlugin(Plugin):
for quality in self.all(): for quality in self.all():
# Last check on resolution only # Check width resolution, range 20
if quality.get('width', 480) == extra.get('resolution_width', 0): if (quality.get('width', 720) - 20) <= extra.get('resolution_width', 0) <= (quality.get('width', 720) + 20):
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0))) log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 720), extra.get('resolution_width', 0)))
return self.setCache(hash, quality)
# Check height resolution, range 20
if (quality.get('height', 480) - 20) <= extra.get('resolution_height', 0) <= (quality.get('height', 480) + 20):
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height', 480), 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:
+3
View File
@@ -133,6 +133,9 @@ class Release(Plugin):
db.delete(release_file) db.delete(release_file)
db.commit() db.commit()
if len(rel.files) == 0:
self.delete(id)
return True return True
return False return False
@@ -133,13 +133,6 @@ config = [{
'type': 'choice', 'type': 'choice',
'options': rename_options 'options': rename_options
}, },
{
'name': 'trailer_name',
'label': 'Trailer naming',
'default': '<filename>-trailer.<ext>',
'type': 'choice',
'options': rename_options
},
], ],
}, },
], ],
+10 -2
View File
@@ -313,7 +313,10 @@ class Renamer(Plugin):
elif release.status_id is snatched_status.get('id'): elif release.status_id is snatched_status.get('id'):
if release.quality.id is group['meta_data']['quality']['id']: if release.quality.id is group['meta_data']['quality']['id']:
log.debug('Marking release as downloaded') log.debug('Marking release as downloaded')
release.status_id = downloaded_status.get('id') try:
release.status_id = downloaded_status.get('id')
except Exception, e:
log.error('Failed marking release as finished: %s %s', (e, traceback.format_exc()))
db.commit() db.commit()
# Remove leftover files # Remove leftover files
@@ -337,6 +340,7 @@ class Renamer(Plugin):
log.info('Removing "%s"', src) log.info('Removing "%s"', src)
try: try:
src = ss(src)
if os.path.isfile(src): if os.path.isfile(src):
os.remove(src) os.remove(src)
@@ -350,7 +354,10 @@ class Renamer(Plugin):
# Delete leftover folder from older releases # Delete leftover folder from older releases
for delete_folder in delete_folders: for delete_folder in delete_folders:
self.deleteEmptyFolder(delete_folder, show_error = False) try:
self.deleteEmptyFolder(delete_folder, show_error = False)
except Exception, e:
log.error('Failed to delete folder: %s %s', (e, traceback.format_exc()))
# Rename all files marked # Rename all files marked
group['renamed_files'] = [] group['renamed_files'] = []
@@ -491,6 +498,7 @@ class Renamer(Plugin):
return string.replace(' ', ' ').replace(' .', '.') return string.replace(' ', ' ').replace(' .', '.')
def deleteEmptyFolder(self, folder, show_error = True): def deleteEmptyFolder(self, folder, show_error = True):
folder = ss(folder)
loge = log.error if show_error else log.debug loge = log.error if show_error else log.debug
for root, dirs, files in os.walk(folder): for root, dirs, files in os.walk(folder):
+2 -2
View File
@@ -421,7 +421,7 @@ class Scanner(Plugin):
if not data['quality']: 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 else 'SD' data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0]) filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
data['group'] = self.getGroup(filename[len(folder):]) data['group'] = self.getGroup(filename[len(folder):])
@@ -775,7 +775,7 @@ class Scanner(Plugin):
return None return None
def findYear(self, text): def findYear(self, text):
matches = re.search('(?P<year>[12]{1}[0-9]{3})', text) matches = re.search('(?P<year>19[0-9]{2}|20[0-9]{2})', text)
if matches: if matches:
return matches.group('year') return matches.group('year')
+2 -2
View File
@@ -27,9 +27,9 @@ class Score(Plugin):
score += sizeScore(nzb['size']) score += sizeScore(nzb['size'])
# Torrents only # Torrents only
if nzb.get('seeds'): if nzb.get('seeders'):
try: try:
score += nzb.get('seeds') / 5 score += nzb.get('seeders') / 5
score += nzb.get('leechers') / 10 score += nzb.get('leechers') / 10
except: except:
pass pass
+1 -1
View File
@@ -116,7 +116,7 @@ def sizeScore(size):
def providerScore(provider): def providerScore(provider):
if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']: if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']:
return 20 return 20
if provider in ['Newznab']: if provider in ['Newznab']:
+28 -22
View File
@@ -3,7 +3,8 @@ 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.request import jsonified, getParam
from couchpotato.core.helpers.variable import md5, getTitle, splitString from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
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
@@ -297,7 +298,7 @@ class Searcher(Plugin):
imdb_results = kwargs.get('imdb_results', False) imdb_results = kwargs.get('imdb_results', False)
retention = Env.setting('retention', section = 'nzb') retention = Env.setting('retention', section = 'nzb')
if nzb.get('seeds') is None and 0 < retention < nzb.get('age', 0): if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0):
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name'])) log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
return False return False
@@ -317,16 +318,16 @@ class Searcher(Plugin):
return False return False
ignored_words = splitString(self.conf('ignored_words').lower()) ignored_words = splitString(self.conf('ignored_words').lower())
blacklisted = list(set(nzb_words) & set(ignored_words)) blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words))
if self.conf('ignored_words') and blacklisted: if self.conf('ignored_words') and blacklisted:
log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted))) log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
return False return False
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic'] pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
for p_tag in pron_tags: pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
if p_tag in nzb_words and p_tag not in movie_words: if pron_words:
log.info('Wrong: %s, probably pr0n', (nzb['name'])) log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False return False
#qualities = fireEvent('quality.all', single = True) #qualities = fireEvent('quality.all', single = True)
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True) preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
@@ -362,20 +363,21 @@ class Searcher(Plugin):
return True return True
# Check if nzb contains imdb link # Check if nzb contains imdb link
if self.checkIMDB([nzb['description']], movie['library']['identifier']): if self.checkIMDB([nzb.get('description', '')], movie['library']['identifier']):
return True return True
for movie_title in movie['library']['titles']: for raw_title in movie['library']['titles']:
movie_words = re.split('\W+', simplifyString(movie_title['title'])) for movie_title in possibleTitles(raw_title['title']):
movie_words = re.split('\W+', simplifyString(movie_title))
if self.correctName(nzb['name'], movie_title['title']): if self.correctName(nzb['name'], movie_title):
# if no IMDB link, at least check year range 1 # if no IMDB link, at least check year range 1
if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1): if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1):
return True return True
# if no IMDB link, at least check year # if no IMDB link, at least check year
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
@@ -444,12 +446,16 @@ class Searcher(Plugin):
def correctName(self, check_name, movie_name): def correctName(self, check_name, movie_name):
check_names = [check_name] check_names = [check_name]
try:
check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
except:
pass
for check_name in check_names: # Match names between "
try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
except: pass
# Match longest name between []
try: check_names.append(max(check_name.split('['), key = len))
except: pass
for check_name in list(set(check_names)):
check_movie = fireEvent('scanner.name_year', check_name, single = True) check_movie = fireEvent('scanner.name_year', check_name, single = True)
try: try:
@@ -24,6 +24,13 @@ config = [{
'type': 'dropdown', 'type': 'dropdown',
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')], 'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
}, },
{
'name': 'name',
'label': 'Naming',
'default': '<filename>-trailer',
'advanced': True,
'description': 'Use <filename> to use above settings.'
},
], ],
}, },
], ],
+5 -4
View File
@@ -19,10 +19,11 @@ class Trailer(Plugin):
trailers = fireEvent('trailer.search', group = group, merge = True) trailers = fireEvent('trailer.search', group = group, merge = True)
if not trailers or trailers == []: if not trailers or trailers == []:
log.info('No trailers found for: %s', getTitle(group['library'])) log.info('No trailers found for: %s', getTitle(group['library']))
return return False
for trailer in trailers.get(self.conf('quality'), []): for trailer in trailers.get(self.conf('quality'), []):
destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer)) filename = self.conf('name').replace('<filename>', group['filename']) + ('.%s' % getExt(trailer))
destination = os.path.join(group['destination_dir'], filename)
if not os.path.isfile(destination): if not os.path.isfile(destination):
fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True) fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
else: else:
@@ -33,5 +34,5 @@ class Trailer(Plugin):
# Download first and break # Download first and break
break break
def getRootName(self, data = {}): return True
return os.path.join(data['destination_dir'], data['filename'])
+116 -16
View File
@@ -1,14 +1,17 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import tryFloat from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
possibleTitles, 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
from couchpotato.environment import Env from couchpotato.environment import Env
from urlparse import urlparse from urlparse import urlparse
import cookielib import cookielib
import json
import re import re
import time import time
import traceback import traceback
import urllib2 import urllib2
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
@@ -54,12 +57,8 @@ class YarrProvider(Provider):
def __init__(self): def __init__(self):
addEvent('provider.belongs_to', self.belongsTo) addEvent('provider.belongs_to', self.belongsTo)
addEvent('%s.search' % self.type, self.search)
addEvent('yarr.search', self.search) addEvent('yarr.search', self.search)
addEvent('nzb.feed', self.feed)
def login(self): def login(self):
try: try:
@@ -96,11 +95,29 @@ class YarrProvider(Provider):
return 'try_next' return 'try_next'
def feed(self):
return []
def search(self, movie, quality): def search(self, movie, quality):
return []
if self.isDisabled():
return []
# Login if needed
if self.urls.get('login') and (not self.login_opener and not self.login()):
log.error('Failed to login to: %s', self.getName())
return []
# Create result container
imdb_result = hasattr(self, '_search')
results = ResultList(self, movie, quality, imdb_result = imdb_result)
# Do search based on imdb id
if imdb_result:
self._search(movie, quality, results)
# Search possible titles
else:
for title in possibleTitles(getTitle(movie['library'])):
self._searchOnTitle(title, movie, quality, results)
return results
def belongsTo(self, url, provider = None, host = None): def belongsTo(self, url, provider = None, host = None):
try: try:
@@ -148,10 +165,93 @@ class YarrProvider(Provider):
return [self.cat_backup_id] return [self.cat_backup_id]
def found(self, new): def getJsonData(self, url, **kwargs):
if not new.get('provider_extra'):
new['provider_extra'] = ''
else:
new['provider_extra'] = ', %s' % new['provider_extra']
log.info('Found: score(%(score)s) on %(provider)s%(provider_extra)s: %(name)s', new) data = self.getCache(md5(url), url, **kwargs)
if data:
try:
return json.loads(data)
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return []
def getRSSData(self, url, **kwargs):
data = self.getCache(md5(url), url, **kwargs)
if data:
try:
data = XMLTree.fromstring(data)
return self.getElements(data, 'channel/item')
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return []
def getHTMLData(self, url, **kwargs):
return self.getCache(md5(url), url, **kwargs)
class ResultList(list):
result_ids = None
provider = None
movie = None
quality = None
def __init__(self, provider, movie, quality, **kwargs):
self.result_ids = []
self.provider = provider
self.movie = movie
self.quality = quality
self.kwargs = kwargs
super(ResultList, self).__init__()
def extend(self, results):
for r in results:
self.append(r)
def append(self, result):
new_result = self.fillResult(result)
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new_result, movie = self.movie, quality = self.quality,
imdb_results = self.kwargs.get('imdb_results', False), single = True)
if is_correct_movie and new_result['id'] not in self.result_ids:
new_result['score'] += fireEvent('score.calculate', new_result, self.movie, single = True)
self.found(new_result)
self.result_ids.append(result['id'])
super(ResultList, self).append(new_result)
def fillResult(self, result):
defaults = {
'id': 0,
'type': self.provider.type,
'provider': self.provider.getName(),
'download': self.provider.download,
'url': '',
'name': '',
'age': 0,
'size': 0,
'description': '',
'score': 0
}
return mergeDicts(defaults, result)
def found(self, new_result):
if not new_result.get('provider_extra'):
new_result['provider_extra'] = ''
else:
new_result['provider_extra'] = ', %s' % new_result['provider_extra']
log.info('Found: score(%(score)s) on %(provider)s%(provider_extra)s: %(name)s', new_result)
@@ -3,6 +3,7 @@ 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 libs.themoviedb import tmdb
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -61,7 +62,12 @@ class TheMovieDb(MovieProvider):
if not results: if not results:
log.debug('Searching for movie: %s', q) log.debug('Searching for movie: %s', q)
raw = tmdb.search(search_string)
raw = None
try:
raw = tmdb.search(search_string)
except:
log.error('Failed searching TMDB for "%s": %s', (search_string, traceback.format_exc()))
results = [] results = []
if raw: if raw:
@@ -0,0 +1,22 @@
from .main import BinSearch
def start():
return BinSearch()
config = [{
'name': 'binsearch',
'groups': [
{
'tab': 'searcher',
'subtab': 'nzb_providers',
'name': 'binsearch',
'description': 'Free provider, less accurate. See <a href="https://www.binsearch.info/">BinSearch</a>',
'options': [
{
'name': 'enabled',
'type': 'enabler',
},
],
},
],
}]
@@ -0,0 +1,99 @@
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.nzb.base import NZBProvider
from couchpotato.environment import Env
import re
import traceback
log = CPLog(__name__)
class BinSearch(NZBProvider):
urls = {
'download': 'https://www.binsearch.info/fcgi/nzb.fcgi?q=%s',
'detail': 'https://www.binsearch.info%s',
'search': 'https://www.binsearch.info/index.php?%s',
}
http_time_between_calls = 4 # Seconds
def _search(self, movie, quality, results):
q = '%s %s' % (movie['library']['identifier'], quality.get('identifier'))
arguments = tryUrlencode({
'q': q,
'm': 'n',
'max': 250,
'adv_age': Env.setting('retention', 'nzb'),
'adv_sort': 'date',
'adv_col': 'on',
'adv_nfo': 'on',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
})
data = self.getHTMLData(self.urls['search'] % arguments)
if data:
try:
html = BeautifulSoup(data)
main_table = html.find('table', attrs = {'id':'r2'})
if not main_table:
return
items = main_table.find_all('tr')
for row in items:
title = row.find('span', attrs = {'class':'s'})
if not title: continue
nzb_id = row.find('input', attrs = {'type':'checkbox'})['name']
info = row.find('span', attrs = {'class':'d'})
size_match = re.search('size:.(?P<size>[0-9\.]+.[GMB]+)', info.text)
def extra_check(item):
parts = re.search('available:.(?P<parts>\d+)./.(?P<total>\d+)', info.text)
total = tryInt(parts.group('total'))
parts = tryInt(parts.group('parts'))
if (total / parts) < 0.95 or ((total / parts) >= 0.95 and not 'par2' in info.text.lower()):
log.info2('Wrong: \'%s\', not complete: %s out of %s', (item['name'], parts, total))
return False
if 'requires password' in info.text.lower():
log.info2('Wrong: \'%s\', passworded', (item['name']))
return False
return True
results.append({
'id': nzb_id,
'name': title.text,
'age': tryInt(re.search('(?P<size>\d+d)', row.find_all('td')[-1:][0].text).group('size')[:-1]),
'size': self.parseSize(size_match.group('size')),
'url': self.urls['download'] % nzb_id,
'detail_url': self.urls['detail'] % info.find('a')['href'],
'extra_check': extra_check
})
except:
log.error('Failed to parse HTML response from BinSearch: %s', traceback.format_exc())
def download(self, url = '', nzb_id = ''):
params = {'action': 'nzb'}
params[nzb_id] = 'on'
try:
return self.urlopen(url, params = params, show_error = False)
except:
log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc()))
return 'try_next'
+15 -38
View File
@@ -1,8 +1,6 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \ from couchpotato.core.helpers.variable import tryInt
simplifyString
from couchpotato.core.helpers.variable import tryInt, getTitle
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
@@ -22,24 +20,20 @@ class FTDWorld(NZBProvider):
'login': 'http://ftdworld.net/index.php', 'login': 'http://ftdworld.net/index.php',
} }
http_time_between_calls = 1 #seconds http_time_between_calls = 3 #seconds
cat_ids = [ cat_ids = [
([4, 11], ['dvdr']), ([4, 11], ['dvdr']),
([1], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']), ([1], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']),
([10, 13, 14], ['bd50', '720p', '1080p']), ([10, 13, 14], ['bd50', '720p', '1080p']),
] ]
cat_backup_id = [1] cat_backup_id = 1
def search(self, movie, quality): def _searchOnTitle(self, title, movie, quality, results):
results = [] q = '"%s" %s' % (title, movie['library']['year'])
if self.isDisabled():
return results
q = '%s %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year']) params = tryUrlencode({
params = {
'ctitle': q, 'ctitle': q,
'customQuery': 'usr', 'customQuery': 'usr',
'cage': Env.setting('retention', 'nzb'), 'cage': Env.setting('retention', 'nzb'),
@@ -47,10 +41,9 @@ class FTDWorld(NZBProvider):
'csizemax': quality.get('size_max'), 'csizemax': quality.get('size_max'),
'ccategory': 14, 'ccategory': 14,
'ctype': ','.join([str(x) for x in self.getCatId(quality['identifier'])]), 'ctype': ','.join([str(x) for x in self.getCatId(quality['identifier'])]),
} })
cache_key = 'ftdworld.%s.%s' % (movie['library']['identifier'], q) data = self.getHTMLData(self.urls['search'] % params, opener = self.login_opener)
data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params), opener = self.login_opener)
if data: if data:
try: try:
@@ -59,7 +52,7 @@ class FTDWorld(NZBProvider):
main_table = html.find('table', attrs = {'id':'ftdresult'}) main_table = html.find('table', attrs = {'id':'ftdresult'})
if not main_table: if not main_table:
return results return
items = main_table.find_all('tr', attrs = {'class': re.compile('tcontent')}) items = main_table.find_all('tr', attrs = {'class': re.compile('tcontent')})
@@ -70,34 +63,18 @@ class FTDWorld(NZBProvider):
up = item.find('img', attrs = {'src': re.compile('up.png')}) up = item.find('img', attrs = {'src': re.compile('up.png')})
down = item.find('img', attrs = {'src': re.compile('down.png')}) down = item.find('img', attrs = {'src': re.compile('down.png')})
new = { results.append({
'id': nzb_id, 'id': nzb_id,
'type': 'nzb',
'provider': self.getName(),
'name': toUnicode(item.find('a', attrs = {'href': re.compile('./spotinfo')}).text.strip()), 'name': toUnicode(item.find('a', attrs = {'href': re.compile('./spotinfo')}).text.strip()),
'age': self.calculateAge(int(time.mktime(parse(tds[2].text).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(tds[2].text).timetuple()))),
'size': 0,
'url': self.urls['download'] % nzb_id, 'url': self.urls['download'] % nzb_id,
'download': self.loginDownload, 'download': self.loginDownload,
'detail_url': self.urls['detail'] % nzb_id, 'detail_url': self.urls['detail'] % nzb_id,
'description': '', 'score': (tryInt(up.attrs['title'].split(' ')[0]) * 3) - (tryInt(down.attrs['title'].split(' ')[0]) * 3) if up else 0,
'score': (tryInt(up.attrs['title'].split(' ')[0]) * 3) - (tryInt(down.attrs['title'].split(' ')[0]) * 3), })
}
is_correct_movie = fireEvent('searcher.correct_movie', except:
nzb = new, movie = movie, quality = quality, log.error('Failed to parse HTML response from FTDWorld')
imdb_results = False, single = True)
if is_correct_movie:
new['score'] += fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
return results
except SyntaxError:
log.error('Failed to parse XML response from NZBClub')
return results
def getLoginParams(self): def getLoginParams(self):
return tryUrlencode({ return tryUrlencode({
+32 -114
View File
@@ -1,8 +1,8 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import cleanHost, splitString from couchpotato.core.helpers.variable import cleanHost, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import ResultList
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 from dateutil.parser import parse
@@ -10,7 +10,6 @@ from urllib2 import HTTPError
from urlparse import urlparse from urlparse import urlparse
import time import time
import traceback import traceback
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
@@ -25,140 +24,59 @@ class Newznab(NZBProvider, RSS):
limits_reached = {} limits_reached = {}
cat_ids = [
([2010], ['dvdr']),
([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
([2040], ['720p', '1080p']),
([2050], ['bd50']),
]
cat_backup_id = 2000
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds
def feed(self):
hosts = self.getHosts()
results = []
for host in hosts:
result = self.singleFeed(host)
if result:
results.extend(result)
return results
def singleFeed(self, host):
results = []
if self.isDisabled(host):
return results
arguments = tryUrlencode({
't': self.cat_backup_id,
'r': host['api_key'],
'i': 58,
})
url = "%s?%s" % (cleanHost(host['host']) + 'rss', arguments)
cache_key = 'newznab.%s.feed.%s' % (host['host'], arguments)
results = self.createItems(url, cache_key, host, for_feed = True)
return results
def search(self, movie, quality): def search(self, movie, quality):
hosts = self.getHosts() hosts = self.getHosts()
results = [] results = ResultList(self, movie, quality, imdb_result = True)
for host in hosts:
result = self.singleSearch(host, movie, quality)
if result: for host in hosts:
results.extend(result) if self.isDisabled(host):
continue
self._searchOnHost(host, movie, quality, results)
return results return results
def singleSearch(self, host, movie, quality): def _searchOnHost(self, host, movie, quality, results):
results = []
if self.isDisabled(host):
return results
cat_id = self.getCatId(quality['identifier'])
arguments = tryUrlencode({ arguments = tryUrlencode({
'imdbid': movie['library']['identifier'].replace('tt', ''), 'imdbid': movie['library']['identifier'].replace('tt', ''),
'cat': cat_id[0],
'apikey': host['api_key'], 'apikey': host['api_key'],
'extended': 1 'extended': 1
}) })
url = "%s&%s" % (self.getUrl(host['host'], self.urls['search']), arguments) url = '%s&%s' % (self.getUrl(host['host'], self.urls['search']), arguments)
cache_key = 'newznab.%s.%s.%s' % (host['host'], movie['library']['identifier'], cat_id[0]) nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
results = self.createItems(url, cache_key, host, movie = movie, quality = quality) for nzb in nzbs:
return results date = None
for item in nzb:
if item.attrib.get('name') == 'usenetdate':
date = item.attrib.get('value')
break
def createItems(self, url, cache_key, host, movie = None, quality = None, for_feed = False): if not date:
results = [] date = self.getTextElement(nzb, 'pubDate')
data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) nzb_id = self.getTextElement(nzb, 'guid').split('/')[-1:].pop()
if data: name = self.getTextElement(nzb, 'title')
try:
try:
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
log.debug('%s, %s', (self.getName(), e))
return results
results = [] if not name:
for nzb in nzbs: continue
date = '' results.append({
size = 0 'id': nzb_id,
for item in nzb: 'provider_extra': host['host'],
if item.attrib.get('name') == 'size': 'name': self.getTextElement(nzb, 'title'),
size = item.attrib.get('value') 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
elif item.attrib.get('name') == 'usenetdate': 'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
date = item.attrib.get('value') 'url': (self.getUrl(host['host'], self.urls['download']) % nzb_id) + self.getApiExt(host),
'detail_url': '%sdetails/%s' % (cleanHost(host['host']), nzb_id),
if date is '': log.debug('Date not parsed properly or not available for %s: %s', (host['host'], self.getTextElement(nzb, "title"))) 'content': self.getTextElement(nzb, 'description'),
if size is 0: log.debug('Size not parsed properly or not available for %s: %s', (host['host'], self.getTextElement(nzb, "title"))) })
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
new = {
'id': id,
'provider': self.getName(),
'provider_extra': host['host'],
'type': 'nzb',
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(size) / 1024 / 1024,
'url': (self.getUrl(host['host'], self.urls['download']) % id) + self.getApiExt(host),
'download': self.download,
'detail_url': '%sdetails/%s' % (cleanHost(host['host']), id),
'content': self.getTextElement(nzb, "description"),
}
if not for_feed:
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = True, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
else:
results.append(new)
return results
except SyntaxError:
log.error('Failed to parse XML response from Newznab: %s', host)
return results
def getHosts(self): def getHosts(self):
+29 -65
View File
@@ -1,15 +1,11 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
simplifyString
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt, getTitle 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 dateutil.parser import parse from dateutil.parser import parse
import time import time
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
@@ -22,80 +18,48 @@ class NZBClub(NZBProvider, RSS):
http_time_between_calls = 4 #seconds http_time_between_calls = 4 #seconds
def search(self, movie, quality): def _searchOnTitle(self, title, movie, quality, results):
results = [] q = '"%s %s" %s' % (title, movie['library']['year'], quality.get('identifier'))
if self.isDisabled():
return results
q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier')) params = tryUrlencode({
params = {
'q': q, 'q': q,
'ig': '1', 'ig': '1',
'rpp': 200, 'rpp': 200,
'st': 1, 'st': 1,
'sp': 1, 'sp': 1,
'ns': 1, 'ns': 1,
} })
cache_key = 'nzbclub.%s.%s.%s' % (movie['library']['identifier'], quality.get('identifier'), q) nzbs = self.getRSSData(self.urls['search'] % params)
data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
if data:
try:
try:
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs: for nzb in nzbs:
nzbclub_id = tryInt(self.getTextElement(nzb, "link").split('/nzb_view/')[1].split('/')[0]) nzbclub_id = tryInt(self.getTextElement(nzb, "link").split('/nzb_view/')[1].split('/')[0])
enclosure = self.getElement(nzb, "enclosure").attrib enclosure = self.getElement(nzb, "enclosure").attrib
size = enclosure['length'] size = enclosure['length']
date = self.getTextElement(nzb, "pubDate") date = self.getTextElement(nzb, "pubDate")
def extra_check(item): def extra_check(item):
full_description = self.getCache('nzbclub.%s' % nzbclub_id, item['detail_url'], cache_timeout = 25920000) full_description = self.getCache('nzbclub.%s' % nzbclub_id, item['detail_url'], cache_timeout = 25920000)
for ignored in ['ARCHIVE inside ARCHIVE', 'Incomplete', 'repair impossible']: for ignored in ['ARCHIVE inside ARCHIVE', 'Incomplete', 'repair impossible']:
if ignored in full_description: if ignored in full_description:
log.info('Wrong: Seems to be passworded or corrupted files: %s', new['name']) log.info('Wrong: Seems to be passworded or corrupted files: %s', item['name'])
return False return False
return True return True
new = { results.append({
'id': nzbclub_id, 'id': nzbclub_id,
'type': 'nzb', 'name': toUnicode(self.getTextElement(nzb, "title")),
'provider': self.getName(), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'name': toUnicode(self.getTextElement(nzb, "title")), 'size': tryInt(size) / 1024 / 1024,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'url': enclosure['url'].replace(' ', '_'),
'size': tryInt(size) / 1024 / 1024, 'detail_url': self.getTextElement(nzb, "link"),
'url': enclosure['url'].replace(' ', '_'), 'get_more_info': self.getMoreInfo,
'download': self.download, 'extra_check': extra_check
'detail_url': self.getTextElement(nzb, "link"), })
'description': '',
'get_more_info': self.getMoreInfo,
'extra_check': extra_check
}
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = False, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
return results
except SyntaxError:
log.error('Failed to parse XML response from NZBClub')
return results
def getMoreInfo(self, item): def getMoreInfo(self, item):
full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000) full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
+31 -72
View File
@@ -1,17 +1,13 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
simplifyString
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt, getTitle 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 from dateutil.parser import parse
import re import re
import time import time
import traceback
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
@@ -20,18 +16,14 @@ class NzbIndex(NZBProvider, RSS):
urls = { urls = {
'download': 'https://www.nzbindex.com/download/', 'download': 'https://www.nzbindex.com/download/',
'api': 'https://www.nzbindex.com/rss/', 'search': 'https://www.nzbindex.com/rss/?%s',
} }
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds
def search(self, movie, quality): def _searchOnTitle(self, title, movie, quality, results):
results = [] q = '"%s" %s %s' % (title, movie['library']['year'], quality.get('identifier'))
if self.isDisabled():
return results
q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
arguments = tryUrlencode({ arguments = tryUrlencode({
'q': q, 'q': q,
'age': Env.setting('retention', 'nzb'), 'age': Env.setting('retention', 'nzb'),
@@ -43,68 +35,37 @@ class NzbIndex(NZBProvider, RSS):
'more': 1, 'more': 1,
'complete': 1, 'complete': 1,
}) })
url = "%s?%s" % (self.urls['api'], arguments)
cache_key = 'nzbindex.%s.%s' % (movie['library']['identifier'], quality.get('identifier')) nzbs = self.getRSSData(self.urls['search'] % arguments)
for nzb in nzbs:
enclosure = self.getElement(nzb, 'enclosure').attrib
nzbindex_id = int(self.getTextElement(nzb, "link").split('/')[4])
data = self.getCache(cache_key, url)
if data:
try: try:
try: description = self.getTextElement(nzb, "description")
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs:
enclosure = self.getElement(nzb, 'enclosure').attrib
nzbindex_id = int(self.getTextElement(nzb, "link").split('/')[4])
try:
description = self.getTextElement(nzb, "description")
except:
description = ''
def extra_check(new):
if '#c20000' in new['description'].lower():
log.info('Wrong: Seems to be passworded: %s', new['name'])
return False
return True
new = {
'id': nzbindex_id,
'type': 'nzb',
'provider': self.getName(),
'download': self.download,
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024,
'url': enclosure['url'],
'detail_url': enclosure['url'].replace('/download/', '/release/'),
'description': description,
'get_more_info': self.getMoreInfo,
'extra_check': extra_check,
'check_nzb': True,
}
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = False, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
return results
except: except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) description = ''
return results def extra_check(item):
if '#c20000' in item['description'].lower():
log.info('Wrong: Seems to be passworded: %s', item['name'])
return False
return True
results.append({
'id': nzbindex_id,
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024,
'url': enclosure['url'],
'detail_url': enclosure['url'].replace('/download/', '/release/'),
'description': description,
'get_more_info': self.getMoreInfo,
'extra_check': extra_check,
})
def getMoreInfo(self, item): def getMoreInfo(self, item):
try: try:
@@ -116,5 +77,3 @@ class NzbIndex(NZBProvider, RSS):
except: except:
pass pass
def isEnabled(self):
return NZBProvider.isEnabled(self) and self.conf('enabled')
+19 -58
View File
@@ -1,11 +1,9 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
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
import time import time
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
@@ -23,15 +21,9 @@ class Nzbsrus(NZBProvider, RSS):
] ]
cat_backup_id = 240 cat_backup_id = 240
def search(self, movie, quality): def _search(self, movie, quality, results):
results = []
if self.isDisabled():
return results
cat_id_string = '&'.join(['c%s=1' % x for x in self.getCatId(quality.get('identifier'))]) cat_id_string = '&'.join(['c%s=1' % x for x in self.getCatId(quality.get('identifier'))])
arguments = tryUrlencode({ arguments = tryUrlencode({
'searchtext': 'imdb:' + movie['library']['identifier'][2:], 'searchtext': 'imdb:' + movie['library']['identifier'][2:],
'uid': self.conf('userid'), 'uid': self.conf('userid'),
@@ -42,60 +34,29 @@ class Nzbsrus(NZBProvider, RSS):
# check for english_only # check for english_only
if self.conf('english_only'): if self.conf('english_only'):
arguments += "&lang0=1&lang3=1&lang1=1" arguments += '&lang0=1&lang3=1&lang1=1'
url = "%s&%s&%s" % (self.urls['search'], arguments , cat_id_string) url = '%s&%s&%s' % (self.urls['search'], arguments , cat_id_string)
nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
cache_key = 'nzbsrus_1.%s.%s' % (movie['library'].get('identifier'), cat_id_string) for nzb in nzbs:
single_cat = True
data = self.getCache(cache_key, url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()}) title = self.getTextElement(nzb, 'name')
if data: if 'error' in title.lower(): continue
try:
try:
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'results/result')
except Exception, e:
log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs: nzb_id = self.getTextElement(nzb, 'id')
size = int(round(int(self.getTextElement(nzb, 'size')) / 1048576))
age = int(round((time.time() - int(self.getTextElement(nzb, 'postdate'))) / 86400))
title = self.getTextElement(nzb, "name") results.append({
if 'error' in title.lower(): continue 'id': nzb_id,
'name': title,
id = self.getTextElement(nzb, "id") 'age': age,
size = int(round(int(self.getTextElement(nzb, "size")) / 1048576)) 'size': size,
age = int(round((time.time() - int(self.getTextElement(nzb, "postdate"))) / 86400)) 'url': self.urls['download'] % id + self.getApiExt() + self.getTextElement(nzb, 'key'),
'detail_url': self.urls['detail'] % nzb_id,
new = { 'description': self.getTextElement(nzb, 'addtext'),
'id': id, })
'type': 'nzb',
'provider': self.getName(),
'name': title,
'age': age,
'size': size,
'url': self.urls['download'] % id + self.getApiExt() + self.getTextElement(nzb, "key"),
'download': self.download,
'detail_url': self.urls['detail'] % id,
'description': self.getTextElement(nzb, "addtext"),
'check_nzb': True,
}
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = True, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
return results
except SyntaxError:
log.error('Failed to parse XML response from Nzbsrus.com')
return results
def getApiExt(self): def getApiExt(self):
return '/%s/' % (self.conf('userid')) return '/%s/' % (self.conf('userid'))
@@ -0,0 +1,23 @@
from .main import Nzbx
def start():
return Nzbx()
config = [{
'name': 'nzbx',
'groups': [
{
'tab': 'searcher',
'subtab': 'nzb_providers',
'name': 'nzbX',
'description': 'Free provider, less accurate. See <a href="https://www.nzbx.co/">nzbX</a>',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': True,
},
],
},
],
}]
@@ -0,0 +1,37 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
log = CPLog(__name__)
class Nzbx(NZBProvider):
urls = {
'search': 'https://nzbx.co/api/search?%s',
'details': 'https://nzbx.co/api/details?guid=%s',
}
http_time_between_calls = 1 # Seconds
def _search(self, movie, quality, results):
# Get nbzs
arguments = tryUrlencode({
'q': movie['library']['identifier'].replace('tt', ''),
'sf': quality.get('size_min'),
})
nzbs = self.getJsonData(self.urls['search'] % arguments)
for nzb in nzbs:
results.append({
'id': nzb['guid'],
'url': nzb['nzb'],
'detail_url': self.urls['details'] % nzb['guid'],
'name': nzb['name'],
'age': self.calculateAge(int(nzb['postdate'])),
'size': tryInt(nzb['size']) / 1024 / 1024,
'score': 5 if nzb['votes']['upvotes'] > nzb['votes']['downvotes'] else 0
})
@@ -0,0 +1,31 @@
from .main import OMGWTFNZBs
def start():
return OMGWTFNZBs()
config = [{
'name': 'omgwtfnzbs',
'groups': [
{
'tab': 'searcher',
'subtab': 'nzb_providers',
'name': 'OMGWTFNZBs',
'description': 'See <a href="http://www.omgwtfnzbs.com/">OMGWTFNZBs</a>',
'options': [
{
'name': 'enabled',
'type': 'enabler',
},
{
'name': 'username',
'default': '',
},
{
'name': 'api_key',
'label': 'Api Key',
'default': '',
},
],
},
],
}]
@@ -0,0 +1,61 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse
from urlparse import urlparse, parse_qs
import time
log = CPLog(__name__)
class OMGWTFNZBs(NZBProvider, RSS):
urls = {
'search': 'http://rss.omgwtfnzbs.com/rss-search.php?%s',
}
http_time_between_calls = 1 #seconds
cat_ids = [
([15], ['dvdrip']),
([15, 16], ['brrip']),
([16], ['720p', '1080p', 'bd50']),
([17], ['dvdr']),
]
cat_backup_id = 'movie'
def search(self, movie, quality):
if quality['identifier'] in fireEvent('quality.pre_releases', single = True):
return []
return super(OMGWTFNZBs, self).search(movie, quality)
def _searchOnTitle(self, title, movie, quality, results):
q = '%s %s' % (title, movie['library']['year'])
params = tryUrlencode({
'search': q,
'catid': ','.join([str(x) for x in self.getCatId(quality['identifier'])]),
'user': self.conf('username', default = ''),
'api': self.conf('api_key', default = ''),
})
nzbs = self.getRSSData(self.urls['search'] % params)
for nzb in nzbs:
enclosure = self.getElement(nzb, 'enclosure').attrib
results.append({
'id': parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0],
'name': toUnicode(self.getTextElement(nzb, 'title')),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, 'pubDate')).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024,
'url': enclosure['url'],
'detail_url': self.getTextElement(nzb, 'link'),
'description': self.getTextElement(nzb, 'description')
})
@@ -24,3 +24,9 @@ class TorrentProvider(YarrProvider):
return getImdb(data) == imdbId return getImdb(data) == imdbId
return False return False
class TorrentMagnetProvider(TorrentProvider):
type = 'torrent_magnet'
download = None
@@ -1,16 +1,14 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import tryInt, getTitle
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 TorrentMagnetProvider
import re import re
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
class KickAssTorrents(TorrentProvider): class KickAssTorrents(TorrentMagnetProvider):
urls = { urls = {
'test': 'https://kat.ph/', 'test': 'https://kat.ph/',
@@ -30,16 +28,10 @@ class KickAssTorrents(TorrentProvider):
http_time_between_calls = 1 #seconds http_time_between_calls = 1 #seconds
cat_backup_id = None cat_backup_id = None
def search(self, movie, quality): def _search(self, movie, quality, results):
results = [] data = self.getHTMLData(self.urls['search'] % ('m', movie['library']['identifier'].replace('tt', '')))
if self.isDisabled():
return results
title = simplifyString(getTitle(movie['library'])).replace(' ', '-')
cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, self.urls['search'] % (title, movie['library']['identifier'].replace('tt', '')))
if data: if data:
cat_ids = self.getCatId(quality['identifier']) cat_ids = self.getCatId(quality['identifier'])
@@ -53,62 +45,42 @@ class KickAssTorrents(TorrentProvider):
continue continue
try: try:
for temp in result.find_all('tr'):
if temp['class'] is 'firstr' or not temp.get('id'):
continue
try: new = {}
for temp in result.find_all('tr'):
if temp['class'] is 'firstr' or not temp.get('id'):
continue
new = { nr = 0
'type': 'torrent_magnet', for td in temp.find_all('td'):
'check_nzb': False, column_name = table_order[nr]
'description': '', if column_name:
'provider': self.getName(),
'score': 0,
}
nr = 0 if column_name is 'name':
for td in temp.find_all('td'): link = td.find('div', {'class': 'torrentname'}).find_all('a')[1]
column_name = table_order[nr] new['id'] = temp.get('id')[-8:]
if column_name: new['name'] = link.text
new['url'] = td.find('a', 'imagnet')['href']
new['detail_url'] = self.urls['detail'] % link['href'][1:]
new['score'] = 20 if td.find('a', 'iverif') else 0
elif column_name is 'size':
new['size'] = self.parseSize(td.text)
elif column_name is 'age':
new['age'] = self.ageToDays(td.text)
elif column_name is 'seeds':
new['seeders'] = tryInt(td.text)
elif column_name is 'leechers':
new['leechers'] = tryInt(td.text)
if column_name is 'name': nr += 1
link = td.find('div', {'class': 'torrentname'}).find_all('a')[1]
new['id'] = temp.get('id')[-8:]
new['name'] = link.text
new['url'] = td.find('a', 'imagnet')['href']
new['detail_url'] = self.urls['detail'] % link['href'][1:]
new['score'] = 20 if td.find('a', 'iverif') else 0
elif column_name is 'size':
new['size'] = self.parseSize(td.text)
elif column_name is 'age':
new['age'] = self.ageToDays(td.text)
elif column_name is 'seeds':
new['seeds'] = tryInt(td.text)
elif column_name is 'leechers':
new['leechers'] = tryInt(td.text)
nr += 1 results.append(new)
new['score'] += fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = True, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
except:
log.error('Failed parsing KickAssTorrents: %s', traceback.format_exc())
except: except:
pass log.error('Failed parsing KickAssTorrents: %s', traceback.format_exc())
return results
except AttributeError: except AttributeError:
log.debug('No search results found.') log.debug('No search results found.')
return results
def ageToDays(self, age_str): def ageToDays(self, age_str):
age = 0 age = 0
age_str = age_str.replace('&nbsp;', ' ') age_str = age_str.replace('&nbsp;', ' ')
@@ -1,4 +1,3 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -65,18 +64,11 @@ class PassThePopcorn(TorrentProvider):
else: else:
raise PassThePopcorn.NotLoggedInHTTPError(req.get_full_url(), code, msg, headers, fp) raise PassThePopcorn.NotLoggedInHTTPError(req.get_full_url(), code, msg, headers, fp)
def search(self, movie, quality): def _search(self, movie, quality, results):
results = []
if self.isDisabled():
return results
movie_title = getTitle(movie['library']) movie_title = getTitle(movie['library'])
quality_id = quality['identifier'] quality_id = quality['identifier']
log.info('Searching for %s at quality %s' % (movie_title, quality_id))
params = mergeDicts(self.quality_search_params[quality_id].copy(), { params = mergeDicts(self.quality_search_params[quality_id].copy(), {
'order_by': 'relevance', 'order_by': 'relevance',
'order_way': 'descending', 'order_way': 'descending',
@@ -85,7 +77,7 @@ class PassThePopcorn(TorrentProvider):
# Do login for the cookies # Do login for the cookies
if not self.login_opener and not self.login(): if not self.login_opener and not self.login():
return results return
try: try:
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params)) url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
@@ -93,12 +85,11 @@ class PassThePopcorn(TorrentProvider):
res = json.loads(txt) res = json.loads(txt)
except: except:
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)' % params) log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)' % params)
return [] return
try: try:
if not 'Movies' in res: if not 'Movies' in res:
log.info("PTP search returned nothing for '%s' at quality '%s' with search parameters %s" % (movie_title, quality_id, params)) return
return []
authkey = res['AuthKey'] authkey = res['AuthKey']
passkey = res['PassKey'] passkey = res['PassKey']
@@ -118,7 +109,6 @@ class PassThePopcorn(TorrentProvider):
if 'Scene' in torrent and torrent['Scene']: if 'Scene' in torrent and torrent['Scene']:
torrentdesc += ' Scene' torrentdesc += ' Scene'
if 'RemasterTitle' in torrent and torrent['RemasterTitle']: if 'RemasterTitle' in torrent and torrent['RemasterTitle']:
# eliminate odd characters...
torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle']) torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle'])
torrentdesc += ' (%s)' % quality_id torrentdesc += ' (%s)' % quality_id
@@ -127,39 +117,23 @@ class PassThePopcorn(TorrentProvider):
def extra_check(item): def extra_check(item):
return self.torrentMeetsQualitySpec(item, type) return self.torrentMeetsQualitySpec(item, type)
def extra_score(item): results.append({
return 50 if torrent['GoldenPopcorn'] else 0
new = {
'id': torrent_id, 'id': torrent_id,
'type': 'torrent',
'provider': self.getName(),
'name': torrent_name, 'name': torrent_name,
'description': '',
'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),
'provider': self.getName(),
'seeders': tryInt(torrent['Seeders']), 'seeders': tryInt(torrent['Seeders']),
'leechers': tryInt(torrent['Leechers']), 'leechers': tryInt(torrent['Leechers']),
'extra_score': extra_score, 'score': 50 if torrent['GoldenPopcorn'] else 0,
'extra_check': extra_check, 'extra_check': extra_check,
'download': self.loginDownload, 'download': self.loginDownload,
} })
new['score'] = fireEvent('score.calculate', new, movie, single = True)
if fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality):
results.append(new)
self.found(new)
return results
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()))
return []
def login(self): def login(self):
cookieprocessor = urllib2.HTTPCookieProcessor(cookielib.CookieJar()) cookieprocessor = urllib2.HTTPCookieProcessor(cookielib.CookieJar())
@@ -10,7 +10,7 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'torrent_providers', 'subtab': 'torrent_providers',
'name': 'PublicHD', 'name': 'PublicHD',
'description': 'Public Torrent site with only HD content. See <a href="https://publichd.eu/">PublicHD</a>', 'description': 'Public Torrent site with only HD content. See <a href="https://publichd.se/">PublicHD</a>',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@@ -1,9 +1,8 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
from couchpotato.core.helpers.variable import getTitle, tryInt from couchpotato.core.helpers.variable import tryInt
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 TorrentMagnetProvider
from urlparse import parse_qs from urlparse import parse_qs
import re import re
import traceback import traceback
@@ -11,31 +10,31 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class PublicHD(TorrentProvider): class PublicHD(TorrentMagnetProvider):
urls = { urls = {
'test': 'https://publichd.eu', 'test': 'https://publichd.se',
'detail': 'https://publichd.eu/index.php?page=torrent-details&id=%s', 'detail': 'https://publichd.se/index.php?page=torrent-details&id=%s',
'search': 'https://publichd.eu/index.php', 'search': 'https://publichd.se/index.php',
} }
http_time_between_calls = 0 http_time_between_calls = 0
def search(self, movie, quality): def search(self, movie, quality):
results = [] if not quality.get('hd', False):
return []
if self.isDisabled() or not quality.get('hd', False): return super(PublicHD, self).search(movie, quality)
return results
def _searchOnTitle(self, title, movie, quality, results):
params = tryUrlencode({ params = tryUrlencode({
'page':'torrents', 'page':'torrents',
'search': '%s %s' % (getTitle(movie['library']), movie['library']['year']), 'search': '%s %s' % (title, movie['library']['year']),
'active': 1, 'active': 1,
}) })
url = '%s?%s' % (self.urls['search'], params)
cache_key = 'publichd.%s.%s' % (movie['library']['identifier'], quality.get('identifier')) data = self.getHTMLData('%s?%s' % (self.urls['search'], params))
data = self.getCache(cache_key, url)
if data: if data:
@@ -53,36 +52,20 @@ class PublicHD(TorrentProvider):
url = parse_qs(info_url['href']) url = parse_qs(info_url['href'])
new = { results.append({
'id': url['id'][0], 'id': url['id'][0],
'name': info_url.string, 'name': info_url.string,
'type': 'torrent_magnet',
'check_nzb': False,
'description': '',
'provider': self.getName(),
'url': download['href'], 'url': download['href'],
'detail_url': self.urls['detail'] % url['id'][0], 'detail_url': self.urls['detail'] % url['id'][0],
'size': self.parseSize(result.find_all('td')[7].string), 'size': self.parseSize(result.find_all('td')[7].string),
'seeders': tryInt(result.find_all('td')[4].string), 'seeders': tryInt(result.find_all('td')[4].string),
'leechers': tryInt(result.find_all('td')[5].string), 'leechers': tryInt(result.find_all('td')[5].string),
'get_more_info': self.getMoreInfo 'get_more_info': self.getMoreInfo
} })
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
imdb_results = False, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
return results
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()))
return []
def getMoreInfo(self, item): def getMoreInfo(self, item):
full_description = self.getCache('publichd.%s' % item['id'], item['detail_url'], cache_timeout = 25920000) full_description = self.getCache('publichd.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
html = BeautifulSoup(full_description) html = BeautifulSoup(full_description)
@@ -1,5 +1,4 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
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
@@ -27,30 +26,24 @@ class SceneAccess(TorrentProvider):
http_time_between_calls = 1 #seconds http_time_between_calls = 1 #seconds
def search(self, movie, quality): def _search(self, movie, quality, results):
results = []
if self.isDisabled():
return results
url = self.urls['search'] % ( url = self.urls['search'] % (
self.getCatId(quality['identifier'])[0], self.getCatId(quality['identifier'])[0],
self.getCatId(quality['identifier'])[0] self.getCatId(quality['identifier'])[0]
) )
q = '%s %s' % (movie['library']['identifier'], quality.get('identifier'))
arguments = tryUrlencode({ arguments = tryUrlencode({
'search': q, 'search': movie['library']['identifier'],
'method': 1, 'method': 1,
}) })
url = "%s&%s" % (url, arguments) url = "%s&%s" % (url, arguments)
# Do login for the cookies # Do login for the cookies
if not self.login_opener and not self.login(): if not self.login_opener and not self.login():
return results return
cache_key = 'sceneaccess.%s.%s' % (movie['library']['identifier'], quality.get('identifier')) data = self.getHTMLData(url, opener = self.login_opener)
data = self.getCache(cache_key, url, opener = self.login_opener)
if data: if data:
html = BeautifulSoup(data) html = BeautifulSoup(data)
@@ -58,7 +51,7 @@ class SceneAccess(TorrentProvider):
try: try:
resultsTable = html.find('table', attrs = {'id' : 'torrents-table'}) resultsTable = html.find('table', attrs = {'id' : 'torrents-table'})
if resultsTable is None: if resultsTable is None:
return results return
entries = resultsTable.find_all('tr', attrs = {'class' : 'tt_row'}) entries = resultsTable.find_all('tr', attrs = {'class' : 'tt_row'})
for result in entries: for result in entries:
@@ -66,38 +59,23 @@ class SceneAccess(TorrentProvider):
link = result.find('td', attrs = {'class' : 'ttr_name'}).find('a') link = result.find('td', attrs = {'class' : 'ttr_name'}).find('a')
url = result.find('td', attrs = {'class' : 'td_dl'}).find('a') url = result.find('td', attrs = {'class' : 'td_dl'}).find('a')
leechers = result.find('td', attrs = {'class' : 'ttr_leechers'}).find('a') leechers = result.find('td', attrs = {'class' : 'ttr_leechers'}).find('a')
id = link['href'].replace('details?id=', '') torrent_id = link['href'].replace('details?id=', '')
new = { results.append({
'id': id, 'id': torrent_id,
'type': 'torrent',
'check_nzb': False,
'description': '',
'provider': self.getName(),
'name': link['title'], 'name': link['title'],
'url': self.urls['download'] % url['href'], 'url': self.urls['download'] % url['href'],
'detail_url': self.urls['detail'] % id, 'detail_url': self.urls['detail'] % torrent_id,
'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, 'download': self.loginDownload,
'get_more_info': self.getMoreInfo, 'get_more_info': self.getMoreInfo,
} })
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
imdb_results = False, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
return results
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()))
return []
def getLoginParams(self): def getLoginParams(self):
return tryUrlencode({ return tryUrlencode({
'username': self.conf('username'), 'username': self.conf('username'),
@@ -1,7 +1,6 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode from couchpotato.core.helpers.encoding import simplifyString, tryUrlencode
from couchpotato.core.helpers.variable import getTitle, tryInt from couchpotato.core.helpers.variable import tryInt
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
import traceback import traceback
@@ -21,13 +20,9 @@ class SceneHD(TorrentProvider):
http_time_between_calls = 1 #seconds http_time_between_calls = 1 #seconds
def search(self, movie, quality): def _searchOnTitle(self, title, movie, quality, results):
results = [] q = '"%s %s" %s' % (simplifyString(title), movie['library']['year'], quality.get('identifier'))
if self.isDisabled():
return results
q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
arguments = tryUrlencode({ arguments = tryUrlencode({
'search': q, 'search': q,
}) })
@@ -35,10 +30,9 @@ class SceneHD(TorrentProvider):
# Cookie login # Cookie login
if not self.login_opener and not self.login(): if not self.login_opener and not self.login():
return results return
cache_key = 'scenehd.%s.%s' % (movie['library']['identifier'], quality.get('identifier')) data = self.getHTMLData(url, opener = self.login_opener)
data = self.getCache(cache_key, url, opener = self.login_opener)
if data: if data:
html = BeautifulSoup(data) html = BeautifulSoup(data)
@@ -52,7 +46,7 @@ class SceneHD(TorrentProvider):
detail_link = all_cells[2].find('a') detail_link = all_cells[2].find('a')
details = detail_link['href'] details = detail_link['href']
id = details.replace('details.php?id=', '') torrent_id = details.replace('details.php?id=', '')
leechers = all_cells[11].find('a') leechers = all_cells[11].find('a')
if leechers: if leechers:
@@ -60,38 +54,20 @@ class SceneHD(TorrentProvider):
else: else:
leechers = all_cells[11].string leechers = all_cells[11].string
new = { results.append({
'id': id, 'id': torrent_id,
'name': detail_link['title'], 'name': detail_link['title'],
'type': 'torrent',
'check_nzb': False,
'description': '',
'provider': self.getName(),
'size': self.parseSize(all_cells[7].string), 'size': self.parseSize(all_cells[7].string),
'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'] % id, 'url': self.urls['download'] % torrent_id,
'download': self.loginDownload, 'download': self.loginDownload,
} 'description': all_cells[1].find('a')['href'],
})
imdb_link = all_cells[1].find('a')
imdb_results = self.imdbMatch(imdb_link['href'], movie['library']['identifier']) if imdb_link else False
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
imdb_results = imdb_results, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
return results
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()))
return []
def getLoginParams(self, params): def getLoginParams(self, params):
return tryUrlencode({ return tryUrlencode({
@@ -1,11 +1,9 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import tryInt, cleanHost
from couchpotato.core.helpers.variable import getTitle, tryInt, cleanHost
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 TorrentMagnetProvider
from couchpotato.environment import Env from couchpotato.environment import Env
from urllib import quote_plus
import re import re
import time import time
import traceback import traceback
@@ -13,7 +11,7 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class ThePirateBay(TorrentProvider): class ThePirateBay(TorrentMagnetProvider):
urls = { urls = {
'detail': '%s/torrent/%s', 'detail': '%s/torrent/%s',
@@ -45,6 +43,58 @@ class ThePirateBay(TorrentProvider):
self.domain = self.conf('domain') self.domain = self.conf('domain')
super(ThePirateBay, self).__init__() super(ThePirateBay, self).__init__()
def _searchOnTitle(self, title, movie, quality, results):
search_url = self.urls['search'] % (self.getDomain(), tryUrlencode(title + ' ' + quality['identifier']), self.getCatId(quality['identifier'])[0])
data = self.getHTMLData(search_url)
if data:
try:
soup = BeautifulSoup(data)
results_table = soup.find('table', attrs = {'id': 'searchResult'})
if not results_table:
return
entries = results_table.find_all('tr')
for result in entries[2:]:
link = result.find(href = re.compile('torrent\/\d+\/'))
download = result.find(href = re.compile('magnet:'))
try:
size = re.search('Size (?P<size>.+),', unicode(result.select('font.detDesc')[0])).group('size')
except:
continue
if link and download:
def extra_score(item):
trusted = (0, 10)[result.find('img', alt = re.compile('Trusted')) != None]
vip = (0, 20)[result.find('img', alt = re.compile('VIP')) != None]
confirmed = (0, 30)[result.find('img', alt = re.compile('Helpers')) != None]
moderated = (0, 50)[result.find('img', alt = re.compile('Moderator')) != None]
return confirmed + trusted + vip + moderated
results.append({
'id': re.search('/(?P<id>\d+)/', link['href']).group('id'),
'name': link.string,
'url': download['href'],
'detail_url': self.getDomain(link['href']),
'size': self.parseSize(size),
'seeders': tryInt(result.find_all('td')[2].string),
'leechers': tryInt(result.find_all('td')[3].string),
'extra_score': extra_score,
'get_more_info': self.getMoreInfo
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def isEnabled(self):
return super(ThePirateBay, self).isEnabled() and self.getDomain()
def getDomain(self, url = ''): def getDomain(self, url = ''):
if not self.domain: if not self.domain:
@@ -74,74 +124,6 @@ class ThePirateBay(TorrentProvider):
return cleanHost(self.domain).rstrip('/') + url return cleanHost(self.domain).rstrip('/') + url
def search(self, movie, quality):
results = []
if self.isDisabled() or not self.getDomain():
return results
cache_key = 'thepiratebay.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
search_url = self.urls['search'] % (self.getDomain(), quote_plus(getTitle(movie['library']) + ' ' + quality['identifier']), self.getCatId(quality['identifier'])[0])
data = self.getCache(cache_key, search_url)
if data:
try:
soup = BeautifulSoup(data)
results_table = soup.find('table', attrs = {'id': 'searchResult'})
if not results_table:
return results
entries = results_table.find_all('tr')
for result in entries[2:]:
link = result.find(href = re.compile('torrent\/\d+\/'))
download = result.find(href = re.compile('magnet:'))
try:
size = re.search('Size (?P<size>.+),', unicode(result.select('font.detDesc')[0])).group('size')
except:
continue
if link and download:
def extra_score(item):
trusted = (0, 10)[result.find('img', alt = re.compile('Trusted')) != None]
vip = (0, 20)[result.find('img', alt = re.compile('VIP')) != None]
confirmed = (0, 30)[result.find('img', alt = re.compile('Helpers')) != None]
moderated = (0, 50)[result.find('img', alt = re.compile('Moderator')) != None]
return confirmed + trusted + vip + moderated
new = {
'id': re.search('/(?P<id>\d+)/', link['href']).group('id'),
'type': 'torrent_magnet',
'name': link.string,
'check_nzb': False,
'description': '',
'provider': self.getName(),
'url': download['href'],
'detail_url': self.getDomain(link['href']),
'size': self.parseSize(size),
'seeders': tryInt(result.find_all('td')[2].string),
'leechers': tryInt(result.find_all('td')[3].string),
'extra_score': extra_score,
'get_more_info': self.getMoreInfo
}
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
imdb_results = False, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
return results
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
return []
def getMoreInfo(self, item): def getMoreInfo(self, item):
full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000) full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
html = BeautifulSoup(full_description) html = BeautifulSoup(full_description)
@@ -0,0 +1,32 @@
from .main import TorrentDay
def start():
return TorrentDay()
config = [{
'name': 'torrentday',
'groups': [
{
'tab': 'searcher',
'subtab': 'torrent_providers',
'name': 'TorrentDay',
'description': 'See <a href="http://www.td.af/">TorrentDay</a>',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
],
},
],
}]
@@ -0,0 +1,61 @@
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
log = CPLog(__name__)
class TorrentDay(TorrentProvider):
urls = {
'test': 'http://www.td.af/',
'login' : 'http://www.td.af/torrents/',
'detail': 'http://www.td.af/details.php?id=%s',
'search': 'http://www.td.af/V3/API/API.php',
'download': 'http://www.td.af/download.php/%s/%s',
}
cat_ids = [
([11], ['720p', '1080p']),
([1, 21, 25], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']),
([3], ['dvdr']),
([5], ['bd50']),
]
http_time_between_calls = 1 #seconds
def _searchOnTitle(self, title, movie, quality, results):
q = '"%s %s"' % (title, movie['library']['year'])
params = {
'/browse.php?': None,
'cata': 'yes',
'jxt': 8,
'jxw': 'b',
'search': q,
}
data = self.getJsonData(self.urls['search'], params = params, opener = self.login_opener)
try: torrents = data.get('Fs', [])[0].get('Cn', {}).get('torrents', [])
except: return
for torrent in torrents:
results.append({
'id': torrent['id'],
'name': torrent['name'],
'url': self.urls['download'] % (torrent['id'], torrent['fname']),
'detail_url': self.urls['detail'] % torrent['id'],
'size': self.parseSize(torrent.get('size')),
'seeders': tryInt(torrent.get('seed')),
'leechers': tryInt(torrent.get('leech')),
'download': self.loginDownload,
})
def getLoginParams(self):
return tryUrlencode({
'username': self.conf('username'),
'password': self.conf('password'),
'submit': 'submit',
})
@@ -1,10 +1,8 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import getTitle, tryInt from couchpotato.core.helpers.variable import tryInt
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 urllib import quote_plus
import traceback import traceback
@@ -14,11 +12,11 @@ log = CPLog(__name__)
class TorrentLeech(TorrentProvider): class TorrentLeech(TorrentProvider):
urls = { urls = {
'test' : 'http://torrentleech.org/', 'test' : 'http://www.torrentleech.org/',
'login' : 'http://torrentleech.org/user/account/login/', 'login' : 'http://www.torrentleech.org/user/account/login/',
'detail' : 'http://torrentleech.org/torrent/%s', 'detail' : 'http://www.torrentleech.org/torrent/%s',
'search' : 'http://torrentleech.org/torrents/browse/index/query/%s/categories/%d', 'search' : 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
'download' : 'http://torrentleech.org%s', 'download' : 'http://www.torrentleech.org%s',
} }
cat_ids = [ cat_ids = [
@@ -32,20 +30,12 @@ class TorrentLeech(TorrentProvider):
] ]
http_time_between_calls = 1 #seconds http_time_between_calls = 1 #seconds
cat_backup_id = None
def search(self, movie, quality): def _searchOnTitle(self, title, movie, quality, results):
results = [] url = self.urls['search'] % (tryUrlencode(title.replace(':', '') + ' ' + quality['identifier']), self.getCatId(quality['identifier'])[0])
if self.isDisabled(): data = self.getHTMLData(url, opener = self.login_opener)
return results
# Cookie login
if not self.login_opener and not self.login():
return results
cache_key = 'torrentleech.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
url = self.urls['search'] % (quote_plus(getTitle(movie['library']).replace(':', '') + ' ' + quality['identifier']), self.getCatId(quality['identifier'])[0])
data = self.getCache(cache_key, url, opener = self.login_opener)
if data: if data:
html = BeautifulSoup(data) html = BeautifulSoup(data)
@@ -53,7 +43,7 @@ class TorrentLeech(TorrentProvider):
try: try:
result_table = html.find('table', attrs = {'id' : 'torrenttable'}) result_table = html.find('table', attrs = {'id' : 'torrenttable'})
if not result_table: if not result_table:
return results return
entries = result_table.find_all('tr') entries = result_table.find_all('tr')
@@ -61,37 +51,22 @@ class TorrentLeech(TorrentProvider):
link = result.find('td', attrs = {'class' : 'name'}).find('a') link = result.find('td', attrs = {'class' : 'name'}).find('a')
url = result.find('td', attrs = {'class' : 'quickdownload'}).find('a') url = result.find('td', attrs = {'class' : 'quickdownload'}).find('a')
details = result.find('td', attrs = {'class' : 'name'}).find('a')
new = { results.append({
'id': link['href'].replace('/torrent/', ''), 'id': link['href'].replace('/torrent/', ''),
'name': link.string, 'name': link.string,
'type': 'torrent',
'check_nzb': False,
'description': '',
'provider': self.getName(),
'url': self.urls['download'] % url['href'], 'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % details['href'],
'download': self.loginDownload, '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),
} })
imdb_results = self.imdbMatch(self.urls['detail'] % new['id'], movie['library']['identifier'])
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
imdb_results = imdb_results, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
return results
except: except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return []
def getLoginParams(self): def getLoginParams(self):
return tryUrlencode({ return tryUrlencode({
'username': self.conf('username'), 'username': self.conf('username'),
+1 -1
View File
@@ -100,7 +100,7 @@ class Release(Entity):
movie = ManyToOne('Movie') movie = ManyToOne('Movie')
status = ManyToOne('Status') status = ManyToOne('Status')
quality = ManyToOne('Quality') quality = ManyToOne('Quality')
files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True) files = ManyToMany('File')
info = OneToMany('ReleaseInfo', cascade = 'all, delete-orphan') info = OneToMany('ReleaseInfo', cascade = 'all, delete-orphan')
def to_dict(self, deep = {}, exclude = []): def to_dict(self, deep = {}, exclude = []):
+1 -1
View File
@@ -189,7 +189,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
version_control(db, repo, version = latest_db_version) version_control(db, repo, version = latest_db_version)
current_db_version = db_version(db, repo) current_db_version = db_version(db, repo)
if current_db_version < latest_db_version and not debug: if current_db_version < latest_db_version and not development:
log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version)) log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version))
upgrade(db, repo) upgrade(db, repo)
+1 -1
View File
@@ -1,5 +1,5 @@
#define MyAppName "CouchPotato" #define MyAppName "CouchPotato"
#define MyAppVer "2.0.3" #define MyAppVer "2.0.4"
[Setup] [Setup]
AppName={#MyAppName} AppName={#MyAppName}
+5 -4
View File
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
""" """
Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net> Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard python 2.3+ This module offers extensions to the standard Python
datetime module. datetime module.
""" """
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>" __author__ = "Tomi Pieviläinen <tomi.pievilainen@iki.fi>"
__license__ = "PSF License" __license__ = "Simplified BSD"
__version__ = "1.5" __version__ = "2.1"
+4 -5
View File
@@ -1,11 +1,10 @@
""" """
Copyright (c) 2003-2007 Gustavo Niemeyer <gustavo@niemeyer.net> Copyright (c) 2003-2007 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard python 2.3+ This module offers extensions to the standard Python
datetime module. datetime module.
""" """
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>" __license__ = "Simplified BSD"
__license__ = "PSF License"
import datetime import datetime
@@ -52,7 +51,7 @@ def easter(year, method=EASTER_WESTERN):
""" """
if not (1 <= method <= 3): if not (1 <= method <= 3):
raise ValueError, "invalid method" raise ValueError("invalid method")
# g - Golden year - 1 # g - Golden year - 1
# c - Century # c - Century
@@ -88,5 +87,5 @@ def easter(year, method=EASTER_WESTERN):
p = i-j+e p = i-j+e
d = 1+(p+27+(p+6)//40)%31 d = 1+(p+27+(p+6)//40)%31
m = 3+(p+26)//30 m = 3+(p+26)//30
return datetime.date(int(y),int(m),int(d)) return datetime.date(int(y), int(m), int(d))
+112 -91
View File
@@ -2,25 +2,27 @@
""" """
Copyright (c) 2003-2007 Gustavo Niemeyer <gustavo@niemeyer.net> Copyright (c) 2003-2007 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard python 2.3+ This module offers extensions to the standard Python
datetime module. datetime module.
""" """
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>" from __future__ import unicode_literals
__license__ = "PSF License" __license__ = "Simplified BSD"
import datetime import datetime
import string import string
import time import time
import sys import collections
import os
try: try:
from cStringIO import StringIO from io import StringIO
except ImportError: except ImportError:
from StringIO import StringIO from io import StringIO
import relativedelta from six import text_type, binary_type, integer_types
import tz
from . import relativedelta
from . import tz
__all__ = ["parse", "parserinfo"] __all__ = ["parse", "parserinfo"]
@@ -39,7 +41,7 @@ __all__ = ["parse", "parserinfo"]
class _timelex(object): class _timelex(object):
def __init__(self, instream): def __init__(self, instream):
if isinstance(instream, basestring): if isinstance(instream, text_type):
instream = StringIO(instream) instream = StringIO(instream)
self.instream = instream self.instream = instream
self.wordchars = ('abcdfeghijklmnopqrstuvwxyz' self.wordchars = ('abcdfeghijklmnopqrstuvwxyz'
@@ -133,12 +135,15 @@ class _timelex(object):
def __iter__(self): def __iter__(self):
return self return self
def next(self): def __next__(self):
token = self.get_token() token = self.get_token()
if token is None: if token is None:
raise StopIteration raise StopIteration
return token return token
def next(self):
return self.__next__() # Python 2.x support
def split(cls, s): def split(cls, s):
return list(cls(s)) return list(cls(s))
split = classmethod(split) split = classmethod(split)
@@ -155,7 +160,7 @@ class _resultbase(object):
for attr in self.__slots__: for attr in self.__slots__:
value = getattr(self, attr) value = getattr(self, attr)
if value is not None: if value is not None:
l.append("%s=%s" % (attr, `value`)) l.append("%s=%s" % (attr, repr(value)))
return "%s(%s)" % (classname, ", ".join(l)) return "%s(%s)" % (classname, ", ".join(l))
def __repr__(self): def __repr__(self):
@@ -176,7 +181,7 @@ class parserinfo(object):
("Fri", "Friday"), ("Fri", "Friday"),
("Sat", "Saturday"), ("Sat", "Saturday"),
("Sun", "Sunday")] ("Sun", "Sunday")]
MONTHS = [("Jan", "January"), MONTHS = [("Jan", "January"),
("Feb", "February"), ("Feb", "February"),
("Mar", "March"), ("Mar", "March"),
("Apr", "April"), ("Apr", "April"),
@@ -184,7 +189,7 @@ class parserinfo(object):
("Jun", "June"), ("Jun", "June"),
("Jul", "July"), ("Jul", "July"),
("Aug", "August"), ("Aug", "August"),
("Sep", "September"), ("Sep", "Sept", "September"),
("Oct", "October"), ("Oct", "October"),
("Nov", "November"), ("Nov", "November"),
("Dec", "December")] ("Dec", "December")]
@@ -197,7 +202,7 @@ class parserinfo(object):
PERTAIN = ["of"] PERTAIN = ["of"]
TZOFFSET = {} TZOFFSET = {}
def __init__(self, dayfirst=False, yearfirst=False): def __init__(self, dayfirst = False, yearfirst = False):
self._jump = self._convert(self.JUMP) self._jump = self._convert(self.JUMP)
self._weekdays = self._convert(self.WEEKDAYS) self._weekdays = self._convert(self.WEEKDAYS)
self._months = self._convert(self.MONTHS) self._months = self._convert(self.MONTHS)
@@ -210,7 +215,7 @@ class parserinfo(object):
self.yearfirst = yearfirst self.yearfirst = yearfirst
self._year = time.localtime().tm_year self._year = time.localtime().tm_year
self._century = self._year//100*100 self._century = self._year // 100 * 100
def _convert(self, lst): def _convert(self, lst):
dct = {} dct = {}
@@ -237,7 +242,7 @@ class parserinfo(object):
def month(self, name): def month(self, name):
if len(name) >= 3: if len(name) >= 3:
try: try:
return self._months[name.lower()]+1 return self._months[name.lower()] + 1
except KeyError: except KeyError:
pass pass
return None return None
@@ -268,7 +273,7 @@ class parserinfo(object):
def convertyear(self, year): def convertyear(self, year):
if year < 100: if year < 100:
year += self._century year += self._century
if abs(year-self._year) >= 50: if abs(year - self._year) >= 50:
if year < self._year: if year < self._year:
year += 100 year += 100
else: else:
@@ -289,18 +294,18 @@ class parserinfo(object):
class parser(object): class parser(object):
def __init__(self, info=None): def __init__(self, info = None):
self.info = info or parserinfo() self.info = info or parserinfo()
def parse(self, timestr, default=None, def parse(self, timestr, default = None,
ignoretz=False, tzinfos=None, ignoretz = False, tzinfos = None,
**kwargs): **kwargs):
if not default: if not default:
default = datetime.datetime.now().replace(hour=0, minute=0, default = datetime.datetime.now().replace(hour = 0, minute = 0,
second=0, microsecond=0) second = 0, microsecond = 0)
res = self._parse(timestr, **kwargs) res = self._parse(timestr, **kwargs)
if res is None: if res is None:
raise ValueError, "unknown string format" raise ValueError("unknown string format")
repl = {} repl = {}
for attr in ["year", "month", "day", "hour", for attr in ["year", "month", "day", "hour",
"minute", "second", "microsecond"]: "minute", "second", "microsecond"]:
@@ -309,29 +314,29 @@ class parser(object):
repl[attr] = value repl[attr] = value
ret = default.replace(**repl) ret = default.replace(**repl)
if res.weekday is not None and not res.day: if res.weekday is not None and not res.day:
ret = ret+relativedelta.relativedelta(weekday=res.weekday) ret = ret + relativedelta.relativedelta(weekday = res.weekday)
if not ignoretz: if not ignoretz:
if callable(tzinfos) or tzinfos and res.tzname in tzinfos: if isinstance(tzinfos, collections.Callable) or tzinfos and res.tzname in tzinfos:
if callable(tzinfos): if isinstance(tzinfos, collections.Callable):
tzdata = tzinfos(res.tzname, res.tzoffset) tzdata = tzinfos(res.tzname, res.tzoffset)
else: else:
tzdata = tzinfos.get(res.tzname) tzdata = tzinfos.get(res.tzname)
if isinstance(tzdata, datetime.tzinfo): if isinstance(tzdata, datetime.tzinfo):
tzinfo = tzdata tzinfo = tzdata
elif isinstance(tzdata, basestring): elif isinstance(tzdata, text_type):
tzinfo = tz.tzstr(tzdata) tzinfo = tz.tzstr(tzdata)
elif isinstance(tzdata, int): elif isinstance(tzdata, integer_types):
tzinfo = tz.tzoffset(res.tzname, tzdata) tzinfo = tz.tzoffset(res.tzname, tzdata)
else: else:
raise ValueError, "offset must be tzinfo subclass, " \ raise ValueError("offset must be tzinfo subclass, " \
"tz string, or int offset" "tz string, or int offset")
ret = ret.replace(tzinfo=tzinfo) ret = ret.replace(tzinfo = tzinfo)
elif res.tzname and res.tzname in time.tzname: elif res.tzname and res.tzname in time.tzname:
ret = ret.replace(tzinfo=tz.tzlocal()) ret = ret.replace(tzinfo = tz.tzlocal())
elif res.tzoffset == 0: elif res.tzoffset == 0:
ret = ret.replace(tzinfo=tz.tzutc()) ret = ret.replace(tzinfo = tz.tzutc())
elif res.tzoffset: elif res.tzoffset:
ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) ret = ret.replace(tzinfo = tz.tzoffset(res.tzname, res.tzoffset))
return ret return ret
class _result(_resultbase): class _result(_resultbase):
@@ -339,7 +344,7 @@ class parser(object):
"hour", "minute", "second", "microsecond", "hour", "minute", "second", "microsecond",
"tzname", "tzoffset"] "tzname", "tzoffset"]
def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False): def _parse(self, timestr, dayfirst = None, yearfirst = None, fuzzy = False):
info = self.info info = self.info
if dayfirst is None: if dayfirst is None:
dayfirst = info.dayfirst dayfirst = info.dayfirst
@@ -374,14 +379,14 @@ class parser(object):
and (i >= len_l or (l[i] != ':' and and (i >= len_l or (l[i] != ':' and
info.hms(l[i]) is None))): info.hms(l[i]) is None))):
# 19990101T23[59] # 19990101T23[59]
s = l[i-1] s = l[i - 1]
res.hour = int(s[:2]) res.hour = int(s[:2])
if len_li == 4: if len_li == 4:
res.minute = int(s[2:]) res.minute = int(s[2:])
elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6): elif len_li == 6 or (len_li > 6 and l[i - 1].find('.') == 6):
# YYMMDD or HHMMSS[.ss] # YYMMDD or HHMMSS[.ss]
s = l[i-1] s = l[i - 1]
if not ymd and l[i-1].find('.') == -1: if not ymd and l[i - 1].find('.') == -1:
ymd.append(info.convertyear(int(s[:2]))) ymd.append(info.convertyear(int(s[:2])))
ymd.append(int(s[2:4])) ymd.append(int(s[2:4]))
ymd.append(int(s[4:])) ymd.append(int(s[4:]))
@@ -392,13 +397,13 @@ class parser(object):
res.second, res.microsecond = _parsems(s[4:]) res.second, res.microsecond = _parsems(s[4:])
elif len_li == 8: elif len_li == 8:
# YYYYMMDD # YYYYMMDD
s = l[i-1] s = l[i - 1]
ymd.append(int(s[:4])) ymd.append(int(s[:4]))
ymd.append(int(s[4:6])) ymd.append(int(s[4:6]))
ymd.append(int(s[6:])) ymd.append(int(s[6:]))
elif len_li in (12, 14): elif len_li in (12, 14):
# YYYYMMDDhhmm[ss] # YYYYMMDDhhmm[ss]
s = l[i-1] s = l[i - 1]
ymd.append(int(s[:4])) ymd.append(int(s[:4]))
ymd.append(int(s[4:6])) ymd.append(int(s[4:6]))
ymd.append(int(s[6:8])) ymd.append(int(s[6:8]))
@@ -407,8 +412,8 @@ class parser(object):
if len_li == 14: if len_li == 14:
res.second = int(s[12:]) res.second = int(s[12:])
elif ((i < len_l and info.hms(l[i]) is not None) or elif ((i < len_l and info.hms(l[i]) is not None) or
(i+1 < len_l and l[i] == ' ' and (i + 1 < len_l and l[i] == ' ' and
info.hms(l[i+1]) is not None)): info.hms(l[i + 1]) is not None)):
# HH[ ]h or MM[ ]m or SS[.ss][ ]s # HH[ ]h or MM[ ]m or SS[.ss][ ]s
if l[i] == ' ': if l[i] == ' ':
i += 1 i += 1
@@ -416,12 +421,12 @@ class parser(object):
while True: while True:
if idx == 0: if idx == 0:
res.hour = int(value) res.hour = int(value)
if value%1: if value % 1:
res.minute = int(60*(value%1)) res.minute = int(60 * (value % 1))
elif idx == 1: elif idx == 1:
res.minute = int(value) res.minute = int(value)
if value%1: if value % 1:
res.second = int(60*(value%1)) res.second = int(60 * (value % 1))
elif idx == 2: elif idx == 2:
res.second, res.microsecond = \ res.second, res.microsecond = \
_parsems(value_repr) _parsems(value_repr)
@@ -441,17 +446,28 @@ class parser(object):
newidx = info.hms(l[i]) newidx = info.hms(l[i])
if newidx is not None: if newidx is not None:
idx = newidx idx = newidx
elif i+1 < len_l and l[i] == ':': elif i == len_l and l[i - 2] == ' ' and info.hms(l[i - 3]) is not None:
# X h MM or X m SS
idx = info.hms(l[i - 3]) + 1
if idx == 1:
res.minute = int(value)
if value % 1:
res.second = int(60 * (value % 1))
elif idx == 2:
res.second, res.microsecond = \
_parsems(value_repr)
i += 1
elif i + 1 < len_l and l[i] == ':':
# HH:MM[:SS[.ss]] # HH:MM[:SS[.ss]]
res.hour = int(value) res.hour = int(value)
i += 1 i += 1
value = float(l[i]) value = float(l[i])
res.minute = int(value) res.minute = int(value)
if value%1: if value % 1:
res.second = int(60*(value%1)) res.second = int(60 * (value % 1))
i += 1 i += 1
if i < len_l and l[i] == ':': if i < len_l and l[i] == ':':
res.second, res.microsecond = _parsems(l[i+1]) res.second, res.microsecond = _parsems(l[i + 1])
i += 2 i += 2
elif i < len_l and l[i] in ('-', '/', '.'): elif i < len_l and l[i] in ('-', '/', '.'):
sep = l[i] sep = l[i]
@@ -467,7 +483,7 @@ class parser(object):
if value is not None: if value is not None:
ymd.append(value) ymd.append(value)
assert mstridx == -1 assert mstridx == -1
mstridx = len(ymd)-1 mstridx = len(ymd) - 1
else: else:
return None return None
i += 1 i += 1
@@ -477,18 +493,18 @@ class parser(object):
value = info.month(l[i]) value = info.month(l[i])
if value is not None: if value is not None:
ymd.append(value) ymd.append(value)
mstridx = len(ymd)-1 mstridx = len(ymd) - 1
assert mstridx == -1 assert mstridx == -1
else: else:
ymd.append(int(l[i])) ymd.append(int(l[i]))
i += 1 i += 1
elif i >= len_l or info.jump(l[i]): elif i >= len_l or info.jump(l[i]):
if i+1 < len_l and info.ampm(l[i+1]) is not None: if i + 1 < len_l and info.ampm(l[i + 1]) is not None:
# 12 am # 12 am
res.hour = int(value) res.hour = int(value)
if res.hour < 12 and info.ampm(l[i+1]) == 1: if res.hour < 12 and info.ampm(l[i + 1]) == 1:
res.hour += 12 res.hour += 12
elif res.hour == 12 and info.ampm(l[i+1]) == 0: elif res.hour == 12 and info.ampm(l[i + 1]) == 0:
res.hour = 0 res.hour = 0
i += 1 i += 1
else: else:
@@ -521,7 +537,7 @@ class parser(object):
if value is not None: if value is not None:
ymd.append(value) ymd.append(value)
assert mstridx == -1 assert mstridx == -1
mstridx = len(ymd)-1 mstridx = len(ymd) - 1
i += 1 i += 1
if i < len_l: if i < len_l:
if l[i] in ('-', '/'): if l[i] in ('-', '/'):
@@ -535,12 +551,12 @@ class parser(object):
i += 1 i += 1
ymd.append(int(l[i])) ymd.append(int(l[i]))
i += 1 i += 1
elif (i+3 < len_l and l[i] == l[i+2] == ' ' elif (i + 3 < len_l and l[i] == l[i + 2] == ' '
and info.pertain(l[i+1])): and info.pertain(l[i + 1])):
# Jan of 01 # Jan of 01
# In this case, 01 is clearly year # In this case, 01 is clearly year
try: try:
value = int(l[i+3]) value = int(l[i + 3])
except ValueError: except ValueError:
# Wrong guess # Wrong guess
pass pass
@@ -585,32 +601,32 @@ class parser(object):
# Check for a numbered timezone # Check for a numbered timezone
if res.hour is not None and l[i] in ('+', '-'): if res.hour is not None and l[i] in ('+', '-'):
signal = (-1,1)[l[i] == '+'] signal = (-1, 1)[l[i] == '+']
i += 1 i += 1
len_li = len(l[i]) len_li = len(l[i])
if len_li == 4: if len_li == 4:
# -0300 # -0300
res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60 res.tzoffset = int(l[i][:2]) * 3600 + int(l[i][2:]) * 60
elif i+1 < len_l and l[i+1] == ':': elif i + 1 < len_l and l[i + 1] == ':':
# -03:00 # -03:00
res.tzoffset = int(l[i])*3600+int(l[i+2])*60 res.tzoffset = int(l[i]) * 3600 + int(l[i + 2]) * 60
i += 2 i += 2
elif len_li <= 2: elif len_li <= 2:
# -[0]3 # -[0]3
res.tzoffset = int(l[i][:2])*3600 res.tzoffset = int(l[i][:2]) * 3600
else: else:
return None return None
i += 1 i += 1
res.tzoffset *= signal res.tzoffset *= signal
# Look for a timezone name between parenthesis # Look for a timezone name between parenthesis
if (i+3 < len_l and if (i + 3 < len_l and
info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and info.jump(l[i]) and l[i + 1] == '(' and l[i + 3] == ')' and
3 <= len(l[i+2]) <= 5 and 3 <= len(l[i + 2]) <= 5 and
not [x for x in l[i+2] not [x for x in l[i + 2]
if x not in string.ascii_uppercase]): if x not in string.ascii_uppercase]):
# -0300 (BRST) # -0300 (BRST)
res.tzname = l[i+2] res.tzname = l[i + 2]
i += 4 i += 4
continue continue
@@ -690,7 +706,12 @@ class parser(object):
return res return res
DEFAULTPARSER = parser() DEFAULTPARSER = parser()
def parse(timestr, parserinfo=None, **kwargs): def parse(timestr, parserinfo = None, **kwargs):
# Python 2.x support: datetimes return their string presentation as
# bytes in 2.x and unicode in 3.x, so it's reasonable to expect that
# the parser will get both kinds. Internally we use unicode only.
if isinstance(timestr, binary_type):
timestr = timestr.decode()
if parserinfo: if parserinfo:
return parser(parserinfo).parse(timestr, **kwargs) return parser(parserinfo).parse(timestr, **kwargs)
else: else:
@@ -743,7 +764,7 @@ class _tzparser(object):
if l[i] in ('+', '-'): if l[i] in ('+', '-'):
# Yes, that's right. See the TZ variable # Yes, that's right. See the TZ variable
# documentation. # documentation.
signal = (1,-1)[l[i] == '+'] signal = (1, -1)[l[i] == '+']
i += 1 i += 1
else: else:
signal = -1 signal = -1
@@ -751,16 +772,16 @@ class _tzparser(object):
if len_li == 4: if len_li == 4:
# -0300 # -0300
setattr(res, offattr, setattr(res, offattr,
(int(l[i][:2])*3600+int(l[i][2:])*60)*signal) (int(l[i][:2]) * 3600 + int(l[i][2:]) * 60) * signal)
elif i+1 < len_l and l[i+1] == ':': elif i + 1 < len_l and l[i + 1] == ':':
# -03:00 # -03:00
setattr(res, offattr, setattr(res, offattr,
(int(l[i])*3600+int(l[i+2])*60)*signal) (int(l[i]) * 3600 + int(l[i + 2]) * 60) * signal)
i += 2 i += 2
elif len_li <= 2: elif len_li <= 2:
# -[0]3 # -[0]3
setattr(res, offattr, setattr(res, offattr,
int(l[i][:2])*3600*signal) int(l[i][:2]) * 3600 * signal)
else: else:
return None return None
i += 1 i += 1
@@ -787,29 +808,29 @@ class _tzparser(object):
x.month = int(l[i]) x.month = int(l[i])
i += 2 i += 2
if l[i] == '-': if l[i] == '-':
value = int(l[i+1])*-1 value = int(l[i + 1]) * -1
i += 1 i += 1
else: else:
value = int(l[i]) value = int(l[i])
i += 2 i += 2
if value: if value:
x.week = value x.week = value
x.weekday = (int(l[i])-1)%7 x.weekday = (int(l[i]) - 1) % 7
else: else:
x.day = int(l[i]) x.day = int(l[i])
i += 2 i += 2
x.time = int(l[i]) x.time = int(l[i])
i += 2 i += 2
if i < len_l: if i < len_l:
if l[i] in ('-','+'): if l[i] in ('-', '+'):
signal = (-1,1)[l[i] == "+"] signal = (-1, 1)[l[i] == "+"]
i += 1 i += 1
else: else:
signal = 1 signal = 1
res.dstoffset = (res.stdoffset+int(l[i]))*signal res.dstoffset = (res.stdoffset + int(l[i])) * signal
elif (l.count(',') == 2 and l[i:].count('/') <= 2 and elif (l.count(',') == 2 and l[i:].count('/') <= 2 and
not [y for x in l[i:] if x not in (',','/','J','M', not [y for x in l[i:] if x not in (',', '/', 'J', 'M',
'.','-',':') '.', '-', ':')
for y in x if y not in "0123456789"]): for y in x if y not in "0123456789"]):
for x in (res.start, res.end): for x in (res.start, res.end):
if l[i] == 'J': if l[i] == 'J':
@@ -829,10 +850,10 @@ class _tzparser(object):
i += 1 i += 1
assert l[i] in ('-', '.') assert l[i] in ('-', '.')
i += 1 i += 1
x.weekday = (int(l[i])-1)%7 x.weekday = (int(l[i]) - 1) % 7
else: else:
# year day (zero based) # year day (zero based)
x.yday = int(l[i])+1 x.yday = int(l[i]) + 1
i += 1 i += 1
@@ -842,17 +863,17 @@ class _tzparser(object):
len_li = len(l[i]) len_li = len(l[i])
if len_li == 4: if len_li == 4:
# -0300 # -0300
x.time = (int(l[i][:2])*3600+int(l[i][2:])*60) x.time = (int(l[i][:2]) * 3600 + int(l[i][2:]) * 60)
elif i+1 < len_l and l[i+1] == ':': elif i + 1 < len_l and l[i + 1] == ':':
# -03:00 # -03:00
x.time = int(l[i])*3600+int(l[i+2])*60 x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60
i += 2 i += 2
if i+1 < len_l and l[i+1] == ':': if i + 1 < len_l and l[i + 1] == ':':
i += 2 i += 2
x.time += int(l[i]) x.time += int(l[i])
elif len_li <= 2: elif len_li <= 2:
# -[0]3 # -[0]3
x.time = (int(l[i][:2])*3600) x.time = (int(l[i][:2]) * 3600)
else: else:
return None return None
i += 1 i += 1
+63 -59
View File
@@ -1,15 +1,16 @@
""" """
Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net> Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard python 2.3+ This module offers extensions to the standard Python
datetime module. datetime module.
""" """
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>" __license__ = "Simplified BSD"
__license__ = "PSF License"
import datetime import datetime
import calendar import calendar
from six import integer_types
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
class weekday(object): class weekday(object):
@@ -42,7 +43,7 @@ class weekday(object):
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
class relativedelta: class relativedelta(object):
""" """
The relativedelta type is based on the specification of the excelent The relativedelta type is based on the specification of the excelent
work done by M.-A. Lemburg in his mx.DateTime extension. However, work done by M.-A. Lemburg in his mx.DateTime extension. However,
@@ -113,10 +114,9 @@ Here is the behavior of operations with relativedelta:
yearday=None, nlyearday=None, yearday=None, nlyearday=None,
hour=None, minute=None, second=None, microsecond=None): hour=None, minute=None, second=None, microsecond=None):
if dt1 and dt2: if dt1 and dt2:
if not isinstance(dt1, datetime.date) or \ if (not isinstance(dt1, datetime.date)) or (not isinstance(dt2, datetime.date)):
not isinstance(dt2, datetime.date): raise TypeError("relativedelta only diffs datetime/date")
raise TypeError, "relativedelta only diffs datetime/date" if not type(dt1) == type(dt2): #isinstance(dt1, type(dt2)):
if type(dt1) is not type(dt2):
if not isinstance(dt1, datetime.datetime): if not isinstance(dt1, datetime.datetime):
dt1 = datetime.datetime.fromordinal(dt1.toordinal()) dt1 = datetime.datetime.fromordinal(dt1.toordinal())
elif not isinstance(dt2, datetime.datetime): elif not isinstance(dt2, datetime.datetime):
@@ -172,7 +172,7 @@ Here is the behavior of operations with relativedelta:
self.second = second self.second = second
self.microsecond = microsecond self.microsecond = microsecond
if type(weekday) is int: if isinstance(weekday, integer_types):
self.weekday = weekdays[weekday] self.weekday = weekdays[weekday]
else: else:
self.weekday = weekday self.weekday = weekday
@@ -185,7 +185,7 @@ Here is the behavior of operations with relativedelta:
if yearday > 59: if yearday > 59:
self.leapdays = -1 self.leapdays = -1
if yday: if yday:
ydayidx = [31,59,90,120,151,181,212,243,273,304,334,366] ydayidx = [31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 366]
for idx, ydays in enumerate(ydayidx): for idx, ydays in enumerate(ydayidx):
if yday <= ydays: if yday <= ydays:
self.month = idx+1 self.month = idx+1
@@ -195,7 +195,7 @@ Here is the behavior of operations with relativedelta:
self.day = yday-ydayidx[idx-1] self.day = yday-ydayidx[idx-1]
break break
else: else:
raise ValueError, "invalid year day (%d)" % yday raise ValueError("invalid year day (%d)" % yday)
self._fix() self._fix()
@@ -242,9 +242,26 @@ Here is the behavior of operations with relativedelta:
else: else:
self.years = 0 self.years = 0
def __radd__(self, other): def __add__(self, other):
if isinstance(other, relativedelta):
return relativedelta(years=other.years+self.years,
months=other.months+self.months,
days=other.days+self.days,
hours=other.hours+self.hours,
minutes=other.minutes+self.minutes,
seconds=other.seconds+self.seconds,
microseconds=other.microseconds+self.microseconds,
leapdays=other.leapdays or self.leapdays,
year=other.year or self.year,
month=other.month or self.month,
day=other.day or self.day,
weekday=other.weekday or self.weekday,
hour=other.hour or self.hour,
minute=other.minute or self.minute,
second=other.second or self.second,
microsecond=other.microsecond or self.microsecond)
if not isinstance(other, datetime.date): if not isinstance(other, datetime.date):
raise TypeError, "unsupported type for add operation" raise TypeError("unsupported type for add operation")
elif self._has_time and not isinstance(other, datetime.datetime): elif self._has_time and not isinstance(other, datetime.datetime):
other = datetime.datetime.fromordinal(other.toordinal()) other = datetime.datetime.fromordinal(other.toordinal())
year = (self.year or other.year)+self.years year = (self.year or other.year)+self.years
@@ -285,48 +302,31 @@ Here is the behavior of operations with relativedelta:
ret += datetime.timedelta(days=jumpdays) ret += datetime.timedelta(days=jumpdays)
return ret return ret
def __radd__(self, other):
return self.__add__(other)
def __rsub__(self, other): def __rsub__(self, other):
return self.__neg__().__radd__(other) return self.__neg__().__radd__(other)
def __add__(self, other):
if not isinstance(other, relativedelta):
raise TypeError, "unsupported type for add operation"
return relativedelta(years=other.years+self.years,
months=other.months+self.months,
days=other.days+self.days,
hours=other.hours+self.hours,
minutes=other.minutes+self.minutes,
seconds=other.seconds+self.seconds,
microseconds=other.microseconds+self.microseconds,
leapdays=other.leapdays or self.leapdays,
year=other.year or self.year,
month=other.month or self.month,
day=other.day or self.day,
weekday=other.weekday or self.weekday,
hour=other.hour or self.hour,
minute=other.minute or self.minute,
second=other.second or self.second,
microsecond=other.second or self.microsecond)
def __sub__(self, other): def __sub__(self, other):
if not isinstance(other, relativedelta): if not isinstance(other, relativedelta):
raise TypeError, "unsupported type for sub operation" raise TypeError("unsupported type for sub operation")
return relativedelta(years=other.years-self.years, return relativedelta(years=self.years-other.years,
months=other.months-self.months, months=self.months-other.months,
days=other.days-self.days, days=self.days-other.days,
hours=other.hours-self.hours, hours=self.hours-other.hours,
minutes=other.minutes-self.minutes, minutes=self.minutes-other.minutes,
seconds=other.seconds-self.seconds, seconds=self.seconds-other.seconds,
microseconds=other.microseconds-self.microseconds, microseconds=self.microseconds-other.microseconds,
leapdays=other.leapdays or self.leapdays, leapdays=self.leapdays or other.leapdays,
year=other.year or self.year, year=self.year or other.year,
month=other.month or self.month, month=self.month or other.month,
day=other.day or self.day, day=self.day or other.day,
weekday=other.weekday or self.weekday, weekday=self.weekday or other.weekday,
hour=other.hour or self.hour, hour=self.hour or other.hour,
minute=other.minute or self.minute, minute=self.minute or other.minute,
second=other.second or self.second, second=self.second or other.second,
microsecond=other.second or self.microsecond) microsecond=self.microsecond or other.microsecond)
def __neg__(self): def __neg__(self):
return relativedelta(years=-self.years, return relativedelta(years=-self.years,
@@ -346,7 +346,7 @@ Here is the behavior of operations with relativedelta:
second=self.second, second=self.second,
microsecond=self.microsecond) microsecond=self.microsecond)
def __nonzero__(self): def __bool__(self):
return not (not self.years and return not (not self.years and
not self.months and not self.months and
not self.days and not self.days and
@@ -366,13 +366,13 @@ Here is the behavior of operations with relativedelta:
def __mul__(self, other): def __mul__(self, other):
f = float(other) f = float(other)
return relativedelta(years=self.years*f, return relativedelta(years=int(self.years*f),
months=self.months*f, months=int(self.months*f),
days=self.days*f, days=int(self.days*f),
hours=self.hours*f, hours=int(self.hours*f),
minutes=self.minutes*f, minutes=int(self.minutes*f),
seconds=self.seconds*f, seconds=int(self.seconds*f),
microseconds=self.microseconds*f, microseconds=int(self.microseconds*f),
leapdays=self.leapdays, leapdays=self.leapdays,
year=self.year, year=self.year,
month=self.month, month=self.month,
@@ -383,6 +383,8 @@ Here is the behavior of operations with relativedelta:
second=self.second, second=self.second,
microsecond=self.microsecond) microsecond=self.microsecond)
__rmul__ = __mul__
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, relativedelta): if not isinstance(other, relativedelta):
return False return False
@@ -415,6 +417,8 @@ Here is the behavior of operations with relativedelta:
def __div__(self, other): def __div__(self, other):
return self.__mul__(1/float(other)) return self.__mul__(1/float(other))
__truediv__ = __div__
def __repr__(self): def __repr__(self):
l = [] l = []
for attr in ["years", "months", "days", "leapdays", for attr in ["years", "months", "days", "leapdays",
@@ -426,7 +430,7 @@ Here is the behavior of operations with relativedelta:
"hour", "minute", "second", "microsecond"]: "hour", "minute", "second", "microsecond"]:
value = getattr(self, attr) value = getattr(self, attr)
if value is not None: if value is not None:
l.append("%s=%s" % (attr, `value`)) l.append("%s=%s" % (attr, repr(value)))
return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
# vim:ts=4:sw=4:et # vim:ts=4:sw=4:et
+72 -57
View File
@@ -1,18 +1,22 @@
""" """
Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net> Copyright (c) 2003-2010 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard python 2.3+ This module offers extensions to the standard Python
datetime module. datetime module.
""" """
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>" __license__ = "Simplified BSD"
__license__ = "PSF License"
import itertools import itertools
import datetime import datetime
import calendar import calendar
import thread try:
import _thread
except ImportError:
import thread as _thread
import sys import sys
from six import advance_iterator, integer_types
__all__ = ["rrule", "rruleset", "rrulestr", __all__ = ["rrule", "rruleset", "rrulestr",
"YEARLY", "MONTHLY", "WEEKLY", "DAILY", "YEARLY", "MONTHLY", "WEEKLY", "DAILY",
"HOURLY", "MINUTELY", "SECONDLY", "HOURLY", "MINUTELY", "SECONDLY",
@@ -22,15 +26,15 @@ __all__ = ["rrule", "rruleset", "rrulestr",
M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30+ M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30+
[7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
M365MASK = list(M366MASK) M365MASK = list(M366MASK)
M29, M30, M31 = range(1,30), range(1,31), range(1,32) M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
MDAY365MASK = list(MDAY366MASK) MDAY365MASK = list(MDAY366MASK)
M29, M30, M31 = range(-29,0), range(-30,0), range(-31,0) M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
NMDAY365MASK = list(NMDAY366MASK) NMDAY365MASK = list(NMDAY366MASK)
M366RANGE = (0,31,60,91,121,152,182,213,244,274,305,335,366) M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
M365RANGE = (0,31,59,90,120,151,181,212,243,273,304,334,365) M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
WDAYMASK = [0,1,2,3,4,5,6]*55 WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
MDAY365MASK = tuple(MDAY365MASK) MDAY365MASK = tuple(MDAY365MASK)
M365MASK = tuple(M365MASK) M365MASK = tuple(M365MASK)
@@ -41,7 +45,7 @@ M365MASK = tuple(M365MASK)
DAILY, DAILY,
HOURLY, HOURLY,
MINUTELY, MINUTELY,
SECONDLY) = range(7) SECONDLY) = list(range(7))
# Imported on demand. # Imported on demand.
easter = None easter = None
@@ -52,7 +56,7 @@ class weekday(object):
def __init__(self, weekday, n=None): def __init__(self, weekday, n=None):
if n == 0: if n == 0:
raise ValueError, "Can't create weekday with n == 0" raise ValueError("Can't create weekday with n == 0")
self.weekday = weekday self.weekday = weekday
self.n = n self.n = n
@@ -79,11 +83,11 @@ class weekday(object):
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
class rrulebase: class rrulebase(object):
def __init__(self, cache=False): def __init__(self, cache=False):
if cache: if cache:
self._cache = [] self._cache = []
self._cache_lock = thread.allocate_lock() self._cache_lock = _thread.allocate_lock()
self._cache_gen = self._iter() self._cache_gen = self._iter()
self._cache_complete = False self._cache_complete = False
else: else:
@@ -112,7 +116,7 @@ class rrulebase:
break break
try: try:
for j in range(10): for j in range(10):
cache.append(gen.next()) cache.append(advance_iterator(gen))
except StopIteration: except StopIteration:
self._cache_gen = gen = None self._cache_gen = gen = None
self._cache_complete = True self._cache_complete = True
@@ -133,13 +137,13 @@ class rrulebase:
else: else:
return list(itertools.islice(self, return list(itertools.islice(self,
item.start or 0, item.start or 0,
item.stop or sys.maxint, item.stop or sys.maxsize,
item.step or 1)) item.step or 1))
elif item >= 0: elif item >= 0:
gen = iter(self) gen = iter(self)
try: try:
for i in range(item+1): for i in range(item+1):
res = gen.next() res = advance_iterator(gen)
except StopIteration: except StopIteration:
raise IndexError raise IndexError
return res return res
@@ -232,7 +236,7 @@ class rrule(rrulebase):
byweekno=None, byweekday=None, byweekno=None, byweekday=None,
byhour=None, byminute=None, bysecond=None, byhour=None, byminute=None, bysecond=None,
cache=False): cache=False):
rrulebase.__init__(self, cache) super(rrule, self).__init__(cache)
global easter global easter
if not dtstart: if not dtstart:
dtstart = datetime.datetime.now().replace(microsecond=0) dtstart = datetime.datetime.now().replace(microsecond=0)
@@ -250,13 +254,13 @@ class rrule(rrulebase):
self._until = until self._until = until
if wkst is None: if wkst is None:
self._wkst = calendar.firstweekday() self._wkst = calendar.firstweekday()
elif type(wkst) is int: elif isinstance(wkst, integer_types):
self._wkst = wkst self._wkst = wkst
else: else:
self._wkst = wkst.weekday self._wkst = wkst.weekday
if bysetpos is None: if bysetpos is None:
self._bysetpos = None self._bysetpos = None
elif type(bysetpos) is int: elif isinstance(bysetpos, integer_types):
if bysetpos == 0 or not (-366 <= bysetpos <= 366): if bysetpos == 0 or not (-366 <= bysetpos <= 366):
raise ValueError("bysetpos must be between 1 and 366, " raise ValueError("bysetpos must be between 1 and 366, "
"or between -366 and -1") "or between -366 and -1")
@@ -280,14 +284,14 @@ class rrule(rrulebase):
# bymonth # bymonth
if not bymonth: if not bymonth:
self._bymonth = None self._bymonth = None
elif type(bymonth) is int: elif isinstance(bymonth, integer_types):
self._bymonth = (bymonth,) self._bymonth = (bymonth,)
else: else:
self._bymonth = tuple(bymonth) self._bymonth = tuple(bymonth)
# byyearday # byyearday
if not byyearday: if not byyearday:
self._byyearday = None self._byyearday = None
elif type(byyearday) is int: elif isinstance(byyearday, integer_types):
self._byyearday = (byyearday,) self._byyearday = (byyearday,)
else: else:
self._byyearday = tuple(byyearday) self._byyearday = tuple(byyearday)
@@ -295,7 +299,7 @@ class rrule(rrulebase):
if byeaster is not None: if byeaster is not None:
if not easter: if not easter:
from dateutil import easter from dateutil import easter
if type(byeaster) is int: if isinstance(byeaster, integer_types):
self._byeaster = (byeaster,) self._byeaster = (byeaster,)
else: else:
self._byeaster = tuple(byeaster) self._byeaster = tuple(byeaster)
@@ -305,7 +309,7 @@ class rrule(rrulebase):
if not bymonthday: if not bymonthday:
self._bymonthday = () self._bymonthday = ()
self._bynmonthday = () self._bynmonthday = ()
elif type(bymonthday) is int: elif isinstance(bymonthday, integer_types):
if bymonthday < 0: if bymonthday < 0:
self._bynmonthday = (bymonthday,) self._bynmonthday = (bymonthday,)
self._bymonthday = () self._bymonthday = ()
@@ -318,7 +322,7 @@ class rrule(rrulebase):
# byweekno # byweekno
if byweekno is None: if byweekno is None:
self._byweekno = None self._byweekno = None
elif type(byweekno) is int: elif isinstance(byweekno, integer_types):
self._byweekno = (byweekno,) self._byweekno = (byweekno,)
else: else:
self._byweekno = tuple(byweekno) self._byweekno = tuple(byweekno)
@@ -326,7 +330,7 @@ class rrule(rrulebase):
if byweekday is None: if byweekday is None:
self._byweekday = None self._byweekday = None
self._bynweekday = None self._bynweekday = None
elif type(byweekday) is int: elif isinstance(byweekday, integer_types):
self._byweekday = (byweekday,) self._byweekday = (byweekday,)
self._bynweekday = None self._bynweekday = None
elif hasattr(byweekday, "n"): elif hasattr(byweekday, "n"):
@@ -340,7 +344,7 @@ class rrule(rrulebase):
self._byweekday = [] self._byweekday = []
self._bynweekday = [] self._bynweekday = []
for wday in byweekday: for wday in byweekday:
if type(wday) is int: if isinstance(wday, integer_types):
self._byweekday.append(wday) self._byweekday.append(wday)
elif not wday.n or freq > MONTHLY: elif not wday.n or freq > MONTHLY:
self._byweekday.append(wday.weekday) self._byweekday.append(wday.weekday)
@@ -358,7 +362,7 @@ class rrule(rrulebase):
self._byhour = (dtstart.hour,) self._byhour = (dtstart.hour,)
else: else:
self._byhour = None self._byhour = None
elif type(byhour) is int: elif isinstance(byhour, integer_types):
self._byhour = (byhour,) self._byhour = (byhour,)
else: else:
self._byhour = tuple(byhour) self._byhour = tuple(byhour)
@@ -368,7 +372,7 @@ class rrule(rrulebase):
self._byminute = (dtstart.minute,) self._byminute = (dtstart.minute,)
else: else:
self._byminute = None self._byminute = None
elif type(byminute) is int: elif isinstance(byminute, integer_types):
self._byminute = (byminute,) self._byminute = (byminute,)
else: else:
self._byminute = tuple(byminute) self._byminute = tuple(byminute)
@@ -378,7 +382,7 @@ class rrule(rrulebase):
self._bysecond = (dtstart.second,) self._bysecond = (dtstart.second,)
else: else:
self._bysecond = None self._bysecond = None
elif type(bysecond) is int: elif isinstance(bysecond, integer_types):
self._bysecond = (bysecond,) self._bysecond = (bysecond,)
else: else:
self._bysecond = tuple(bysecond) self._bysecond = tuple(bysecond)
@@ -716,7 +720,7 @@ class _iterinfo(object):
# days from last year's last week number in # days from last year's last week number in
# this year. # this year.
if -1 not in rr._byweekno: if -1 not in rr._byweekno:
lyearweekday = datetime.date(year-1,1,1).weekday() lyearweekday = datetime.date(year-1, 1, 1).weekday()
lno1wkst = (7-lyearweekday+rr._wkst)%7 lno1wkst = (7-lyearweekday+rr._wkst)%7
lyearlen = 365+calendar.isleap(year-1) lyearlen = 365+calendar.isleap(year-1)
if lno1wkst >= 4: if lno1wkst >= 4:
@@ -768,7 +772,7 @@ class _iterinfo(object):
self.lastmonth = month self.lastmonth = month
def ydayset(self, year, month, day): def ydayset(self, year, month, day):
return range(self.yearlen), 0, self.yearlen return list(range(self.yearlen)), 0, self.yearlen
def mdayset(self, year, month, day): def mdayset(self, year, month, day):
set = [None]*self.yearlen set = [None]*self.yearlen
@@ -823,27 +827,38 @@ class _iterinfo(object):
class rruleset(rrulebase): class rruleset(rrulebase):
class _genitem: class _genitem(object):
def __init__(self, genlist, gen): def __init__(self, genlist, gen):
try: try:
self.dt = gen() self.dt = advance_iterator(gen)
genlist.append(self) genlist.append(self)
except StopIteration: except StopIteration:
pass pass
self.genlist = genlist self.genlist = genlist
self.gen = gen self.gen = gen
def next(self): def __next__(self):
try: try:
self.dt = self.gen() self.dt = advance_iterator(self.gen)
except StopIteration: except StopIteration:
self.genlist.remove(self) self.genlist.remove(self)
def __cmp__(self, other): next = __next__
return cmp(self.dt, other.dt)
def __lt__(self, other):
return self.dt < other.dt
def __gt__(self, other):
return self.dt > other.dt
def __eq__(self, other):
return self.dt == other.dt
def __ne__(self, other):
return self.dt != other.dt
def __init__(self, cache=False): def __init__(self, cache=False):
rrulebase.__init__(self, cache) super(rruleset, self).__init__(cache)
self._rrule = [] self._rrule = []
self._rdate = [] self._rdate = []
self._exrule = [] self._exrule = []
@@ -864,14 +879,14 @@ class rruleset(rrulebase):
def _iter(self): def _iter(self):
rlist = [] rlist = []
self._rdate.sort() self._rdate.sort()
self._genitem(rlist, iter(self._rdate).next) self._genitem(rlist, iter(self._rdate))
for gen in [iter(x).next for x in self._rrule]: for gen in [iter(x) for x in self._rrule]:
self._genitem(rlist, gen) self._genitem(rlist, gen)
rlist.sort() rlist.sort()
exlist = [] exlist = []
self._exdate.sort() self._exdate.sort()
self._genitem(exlist, iter(self._exdate).next) self._genitem(exlist, iter(self._exdate))
for gen in [iter(x).next for x in self._exrule]: for gen in [iter(x) for x in self._exrule]:
self._genitem(exlist, gen) self._genitem(exlist, gen)
exlist.sort() exlist.sort()
lastdt = None lastdt = None
@@ -880,17 +895,17 @@ class rruleset(rrulebase):
ritem = rlist[0] ritem = rlist[0]
if not lastdt or lastdt != ritem.dt: if not lastdt or lastdt != ritem.dt:
while exlist and exlist[0] < ritem: while exlist and exlist[0] < ritem:
exlist[0].next() advance_iterator(exlist[0])
exlist.sort() exlist.sort()
if not exlist or ritem != exlist[0]: if not exlist or ritem != exlist[0]:
total += 1 total += 1
yield ritem.dt yield ritem.dt
lastdt = ritem.dt lastdt = ritem.dt
ritem.next() advance_iterator(ritem)
rlist.sort() rlist.sort()
self._len = total self._len = total
class _rrulestr: class _rrulestr(object):
_freq_map = {"YEARLY": YEARLY, _freq_map = {"YEARLY": YEARLY,
"MONTHLY": MONTHLY, "MONTHLY": MONTHLY,
@@ -932,7 +947,7 @@ class _rrulestr:
ignoretz=kwargs.get("ignoretz"), ignoretz=kwargs.get("ignoretz"),
tzinfos=kwargs.get("tzinfos")) tzinfos=kwargs.get("tzinfos"))
except ValueError: except ValueError:
raise ValueError, "invalid until date" raise ValueError("invalid until date")
def _handle_WKST(self, rrkwargs, name, value, **kwargs): def _handle_WKST(self, rrkwargs, name, value, **kwargs):
rrkwargs["wkst"] = self._weekday_map[value] rrkwargs["wkst"] = self._weekday_map[value]
@@ -959,7 +974,7 @@ class _rrulestr:
if line.find(':') != -1: if line.find(':') != -1:
name, value = line.split(':') name, value = line.split(':')
if name != "RRULE": if name != "RRULE":
raise ValueError, "unknown parameter name" raise ValueError("unknown parameter name")
else: else:
value = line value = line
rrkwargs = {} rrkwargs = {}
@@ -972,9 +987,9 @@ class _rrulestr:
ignoretz=ignoretz, ignoretz=ignoretz,
tzinfos=tzinfos) tzinfos=tzinfos)
except AttributeError: except AttributeError:
raise ValueError, "unknown parameter '%s'" % name raise ValueError("unknown parameter '%s'" % name)
except (KeyError, ValueError): except (KeyError, ValueError):
raise ValueError, "invalid '%s': %s" % (name, value) raise ValueError("invalid '%s': %s" % (name, value))
return rrule(dtstart=dtstart, cache=cache, **rrkwargs) return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
def _parse_rfc(self, s, def _parse_rfc(self, s,
@@ -991,7 +1006,7 @@ class _rrulestr:
unfold = True unfold = True
s = s.upper() s = s.upper()
if not s.strip(): if not s.strip():
raise ValueError, "empty string" raise ValueError("empty string")
if unfold: if unfold:
lines = s.splitlines() lines = s.splitlines()
i = 0 i = 0
@@ -1026,36 +1041,36 @@ class _rrulestr:
name, value = line.split(':', 1) name, value = line.split(':', 1)
parms = name.split(';') parms = name.split(';')
if not parms: if not parms:
raise ValueError, "empty property name" raise ValueError("empty property name")
name = parms[0] name = parms[0]
parms = parms[1:] parms = parms[1:]
if name == "RRULE": if name == "RRULE":
for parm in parms: for parm in parms:
raise ValueError, "unsupported RRULE parm: "+parm raise ValueError("unsupported RRULE parm: "+parm)
rrulevals.append(value) rrulevals.append(value)
elif name == "RDATE": elif name == "RDATE":
for parm in parms: for parm in parms:
if parm != "VALUE=DATE-TIME": if parm != "VALUE=DATE-TIME":
raise ValueError, "unsupported RDATE parm: "+parm raise ValueError("unsupported RDATE parm: "+parm)
rdatevals.append(value) rdatevals.append(value)
elif name == "EXRULE": elif name == "EXRULE":
for parm in parms: for parm in parms:
raise ValueError, "unsupported EXRULE parm: "+parm raise ValueError("unsupported EXRULE parm: "+parm)
exrulevals.append(value) exrulevals.append(value)
elif name == "EXDATE": elif name == "EXDATE":
for parm in parms: for parm in parms:
if parm != "VALUE=DATE-TIME": if parm != "VALUE=DATE-TIME":
raise ValueError, "unsupported RDATE parm: "+parm raise ValueError("unsupported RDATE parm: "+parm)
exdatevals.append(value) exdatevals.append(value)
elif name == "DTSTART": elif name == "DTSTART":
for parm in parms: for parm in parms:
raise ValueError, "unsupported DTSTART parm: "+parm raise ValueError("unsupported DTSTART parm: "+parm)
if not parser: if not parser:
from dateutil import parser from dateutil import parser
dtstart = parser.parse(value, ignoretz=ignoretz, dtstart = parser.parse(value, ignoretz=ignoretz,
tzinfos=tzinfos) tzinfos=tzinfos)
else: else:
raise ValueError, "unsupported property: "+name raise ValueError("unsupported property: "+name)
if (forceset or len(rrulevals) > 1 or if (forceset or len(rrulevals) > 1 or
rdatevals or exrulevals or exdatevals): rdatevals or exrulevals or exdatevals):
if not parser and (rdatevals or exdatevals): if not parser and (rdatevals or exdatevals):
+69 -60
View File
@@ -1,11 +1,12 @@
""" """
Copyright (c) 2003-2007 Gustavo Niemeyer <gustavo@niemeyer.net> Copyright (c) 2003-2007 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard python 2.3+ This module offers extensions to the standard Python
datetime module. datetime module.
""" """
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>" __license__ = "Simplified BSD"
__license__ = "PSF License"
from six import string_types, PY3
import datetime import datetime
import struct import struct
@@ -25,6 +26,19 @@ try:
except (ImportError, OSError): except (ImportError, OSError):
tzwin, tzwinlocal = None, None tzwin, tzwinlocal = None, None
def tzname_in_python2(myfunc):
"""Change unicode output into bytestrings in Python 2
tzname() API changed in Python 3. It used to return bytes, but was changed
to unicode strings
"""
def inner_func(*args, **kwargs):
if PY3:
return myfunc(*args, **kwargs)
else:
return myfunc(*args, **kwargs).encode()
return inner_func
ZERO = datetime.timedelta(0) ZERO = datetime.timedelta(0)
EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal() EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal()
@@ -36,6 +50,7 @@ class tzutc(datetime.tzinfo):
def dst(self, dt): def dst(self, dt):
return ZERO return ZERO
@tzname_in_python2
def tzname(self, dt): def tzname(self, dt):
return "UTC" return "UTC"
@@ -63,6 +78,7 @@ class tzoffset(datetime.tzinfo):
def dst(self, dt): def dst(self, dt):
return ZERO return ZERO
@tzname_in_python2
def tzname(self, dt): def tzname(self, dt):
return self._name return self._name
@@ -75,7 +91,7 @@ class tzoffset(datetime.tzinfo):
def __repr__(self): def __repr__(self):
return "%s(%s, %s)" % (self.__class__.__name__, return "%s(%s, %s)" % (self.__class__.__name__,
`self._name`, repr(self._name),
self._offset.days*86400+self._offset.seconds) self._offset.days*86400+self._offset.seconds)
__reduce__ = object.__reduce__ __reduce__ = object.__reduce__
@@ -100,6 +116,7 @@ class tzlocal(datetime.tzinfo):
else: else:
return ZERO return ZERO
@tzname_in_python2
def tzname(self, dt): def tzname(self, dt):
return time.tzname[self._isdst(dt)] return time.tzname[self._isdst(dt)]
@@ -161,7 +178,7 @@ class _ttinfo(object):
for attr in self.__slots__: for attr in self.__slots__:
value = getattr(self, attr) value = getattr(self, attr)
if value is not None: if value is not None:
l.append("%s=%s" % (attr, `value`)) l.append("%s=%s" % (attr, repr(value)))
return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
def __eq__(self, other): def __eq__(self, other):
@@ -191,16 +208,16 @@ class _ttinfo(object):
class tzfile(datetime.tzinfo): class tzfile(datetime.tzinfo):
# http://www.twinsun.com/tz/tz-link.htm # http://www.twinsun.com/tz/tz-link.htm
# ftp://elsie.nci.nih.gov/pub/tz*.tar.gz # ftp://ftp.iana.org/tz/tz*.tar.gz
def __init__(self, fileobj): def __init__(self, fileobj):
if isinstance(fileobj, basestring): if isinstance(fileobj, string_types):
self._filename = fileobj self._filename = fileobj
fileobj = open(fileobj) fileobj = open(fileobj, 'rb')
elif hasattr(fileobj, "name"): elif hasattr(fileobj, "name"):
self._filename = fileobj.name self._filename = fileobj.name
else: else:
self._filename = `fileobj` self._filename = repr(fileobj)
# From tzfile(5): # From tzfile(5):
# #
@@ -212,8 +229,8 @@ class tzfile(datetime.tzinfo):
# ``standard'' byte order (the high-order byte # ``standard'' byte order (the high-order byte
# of the value is written first). # of the value is written first).
if fileobj.read(4) != "TZif": if fileobj.read(4).decode() != "TZif":
raise ValueError, "magic not found" raise ValueError("magic not found")
fileobj.read(16) fileobj.read(16)
@@ -284,7 +301,7 @@ class tzfile(datetime.tzinfo):
for i in range(typecnt): for i in range(typecnt):
ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
abbr = fileobj.read(charcnt) abbr = fileobj.read(charcnt).decode()
# Then there are tzh_leapcnt pairs of four-byte # Then there are tzh_leapcnt pairs of four-byte
# values, written in standard byte order; the # values, written in standard byte order; the
@@ -360,7 +377,7 @@ class tzfile(datetime.tzinfo):
if not self._trans_list: if not self._trans_list:
self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0] self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0]
else: else:
for i in range(timecnt-1,-1,-1): for i in range(timecnt-1, -1, -1):
tti = self._trans_idx[i] tti = self._trans_idx[i]
if not self._ttinfo_std and not tti.isdst: if not self._ttinfo_std and not tti.isdst:
self._ttinfo_std = tti self._ttinfo_std = tti
@@ -448,6 +465,7 @@ class tzfile(datetime.tzinfo):
# dst offset, so I belive that this wouldn't be the right # dst offset, so I belive that this wouldn't be the right
# way to implement this. # way to implement this.
@tzname_in_python2
def tzname(self, dt): def tzname(self, dt):
if not self._ttinfo_std: if not self._ttinfo_std:
return None return None
@@ -465,11 +483,11 @@ class tzfile(datetime.tzinfo):
def __repr__(self): def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, `self._filename`) return "%s(%s)" % (self.__class__.__name__, repr(self._filename))
def __reduce__(self): def __reduce__(self):
if not os.path.isfile(self._filename): if not os.path.isfile(self._filename):
raise ValueError, "Unpickable %s class" % self.__class__.__name__ raise ValueError("Unpickable %s class" % self.__class__.__name__)
return (self.__class__, (self._filename,)) return (self.__class__, (self._filename,))
class tzrange(datetime.tzinfo): class tzrange(datetime.tzinfo):
@@ -515,6 +533,7 @@ class tzrange(datetime.tzinfo):
else: else:
return ZERO return ZERO
@tzname_in_python2
def tzname(self, dt): def tzname(self, dt):
if self._isdst(dt): if self._isdst(dt):
return self._dst_abbr return self._dst_abbr
@@ -524,7 +543,7 @@ class tzrange(datetime.tzinfo):
def _isdst(self, dt): def _isdst(self, dt):
if not self._start_delta: if not self._start_delta:
return False return False
year = datetime.datetime(dt.year,1,1) year = datetime.datetime(dt.year, 1, 1)
start = year+self._start_delta start = year+self._start_delta
end = year+self._end_delta end = year+self._end_delta
dt = dt.replace(tzinfo=None) dt = dt.replace(tzinfo=None)
@@ -561,7 +580,7 @@ class tzstr(tzrange):
res = parser._parsetz(s) res = parser._parsetz(s)
if res is None: if res is None:
raise ValueError, "unknown string format" raise ValueError("unknown string format")
# Here we break the compatibility with the TZ variable handling. # Here we break the compatibility with the TZ variable handling.
# GMT-3 actually *means* the timezone -3. # GMT-3 actually *means* the timezone -3.
@@ -624,9 +643,9 @@ class tzstr(tzrange):
return relativedelta.relativedelta(**kwargs) return relativedelta.relativedelta(**kwargs)
def __repr__(self): def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, `self._s`) return "%s(%s)" % (self.__class__.__name__, repr(self._s))
class _tzicalvtzcomp: class _tzicalvtzcomp(object):
def __init__(self, tzoffsetfrom, tzoffsetto, isdst, def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
tzname=None, rrule=None): tzname=None, rrule=None):
self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
@@ -690,51 +709,52 @@ class _tzicalvtz(datetime.tzinfo):
else: else:
return ZERO return ZERO
@tzname_in_python2
def tzname(self, dt): def tzname(self, dt):
return self._find_comp(dt).tzname return self._find_comp(dt).tzname
def __repr__(self): def __repr__(self):
return "<tzicalvtz %s>" % `self._tzid` return "<tzicalvtz %s>" % repr(self._tzid)
__reduce__ = object.__reduce__ __reduce__ = object.__reduce__
class tzical: class tzical(object):
def __init__(self, fileobj): def __init__(self, fileobj):
global rrule global rrule
if not rrule: if not rrule:
from dateutil import rrule from dateutil import rrule
if isinstance(fileobj, basestring): if isinstance(fileobj, string_types):
self._s = fileobj self._s = fileobj
fileobj = open(fileobj) fileobj = open(fileobj, 'r') # ical should be encoded in UTF-8 with CRLF
elif hasattr(fileobj, "name"): elif hasattr(fileobj, "name"):
self._s = fileobj.name self._s = fileobj.name
else: else:
self._s = `fileobj` self._s = repr(fileobj)
self._vtz = {} self._vtz = {}
self._parse_rfc(fileobj.read()) self._parse_rfc(fileobj.read())
def keys(self): def keys(self):
return self._vtz.keys() return list(self._vtz.keys())
def get(self, tzid=None): def get(self, tzid=None):
if tzid is None: if tzid is None:
keys = self._vtz.keys() keys = list(self._vtz.keys())
if len(keys) == 0: if len(keys) == 0:
raise ValueError, "no timezones defined" raise ValueError("no timezones defined")
elif len(keys) > 1: elif len(keys) > 1:
raise ValueError, "more than one timezone available" raise ValueError("more than one timezone available")
tzid = keys[0] tzid = keys[0]
return self._vtz.get(tzid) return self._vtz.get(tzid)
def _parse_offset(self, s): def _parse_offset(self, s):
s = s.strip() s = s.strip()
if not s: if not s:
raise ValueError, "empty offset" raise ValueError("empty offset")
if s[0] in ('+', '-'): if s[0] in ('+', '-'):
signal = (-1,+1)[s[0]=='+'] signal = (-1, +1)[s[0]=='+']
s = s[1:] s = s[1:]
else: else:
signal = +1 signal = +1
@@ -743,12 +763,12 @@ class tzical:
elif len(s) == 6: elif len(s) == 6:
return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal
else: else:
raise ValueError, "invalid offset: "+s raise ValueError("invalid offset: "+s)
def _parse_rfc(self, s): def _parse_rfc(self, s):
lines = s.splitlines() lines = s.splitlines()
if not lines: if not lines:
raise ValueError, "empty string" raise ValueError("empty string")
# Unfold # Unfold
i = 0 i = 0
@@ -772,7 +792,7 @@ class tzical:
name, value = line.split(':', 1) name, value = line.split(':', 1)
parms = name.split(';') parms = name.split(';')
if not parms: if not parms:
raise ValueError, "empty property name" raise ValueError("empty property name")
name = parms[0].upper() name = parms[0].upper()
parms = parms[1:] parms = parms[1:]
if invtz: if invtz:
@@ -781,7 +801,7 @@ class tzical:
# Process component # Process component
pass pass
else: else:
raise ValueError, "unknown component: "+value raise ValueError("unknown component: "+value)
comptype = value comptype = value
founddtstart = False founddtstart = False
tzoffsetfrom = None tzoffsetfrom = None
@@ -791,27 +811,21 @@ class tzical:
elif name == "END": elif name == "END":
if value == "VTIMEZONE": if value == "VTIMEZONE":
if comptype: if comptype:
raise ValueError, \ raise ValueError("component not closed: "+comptype)
"component not closed: "+comptype
if not tzid: if not tzid:
raise ValueError, \ raise ValueError("mandatory TZID not found")
"mandatory TZID not found"
if not comps: if not comps:
raise ValueError, \ raise ValueError("at least one component is needed")
"at least one component is needed"
# Process vtimezone # Process vtimezone
self._vtz[tzid] = _tzicalvtz(tzid, comps) self._vtz[tzid] = _tzicalvtz(tzid, comps)
invtz = False invtz = False
elif value == comptype: elif value == comptype:
if not founddtstart: if not founddtstart:
raise ValueError, \ raise ValueError("mandatory DTSTART not found")
"mandatory DTSTART not found"
if tzoffsetfrom is None: if tzoffsetfrom is None:
raise ValueError, \ raise ValueError("mandatory TZOFFSETFROM not found")
"mandatory TZOFFSETFROM not found"
if tzoffsetto is None: if tzoffsetto is None:
raise ValueError, \ raise ValueError("mandatory TZOFFSETFROM not found")
"mandatory TZOFFSETFROM not found"
# Process component # Process component
rr = None rr = None
if rrulelines: if rrulelines:
@@ -825,8 +839,7 @@ class tzical:
comps.append(comp) comps.append(comp)
comptype = None comptype = None
else: else:
raise ValueError, \ raise ValueError("invalid component end: "+value)
"invalid component end: "+value
elif comptype: elif comptype:
if name == "DTSTART": if name == "DTSTART":
rrulelines.append(line) rrulelines.append(line)
@@ -835,40 +848,36 @@ class tzical:
rrulelines.append(line) rrulelines.append(line)
elif name == "TZOFFSETFROM": elif name == "TZOFFSETFROM":
if parms: if parms:
raise ValueError, \ raise ValueError("unsupported %s parm: %s "%(name, parms[0]))
"unsupported %s parm: %s "%(name, parms[0])
tzoffsetfrom = self._parse_offset(value) tzoffsetfrom = self._parse_offset(value)
elif name == "TZOFFSETTO": elif name == "TZOFFSETTO":
if parms: if parms:
raise ValueError, \ raise ValueError("unsupported TZOFFSETTO parm: "+parms[0])
"unsupported TZOFFSETTO parm: "+parms[0]
tzoffsetto = self._parse_offset(value) tzoffsetto = self._parse_offset(value)
elif name == "TZNAME": elif name == "TZNAME":
if parms: if parms:
raise ValueError, \ raise ValueError("unsupported TZNAME parm: "+parms[0])
"unsupported TZNAME parm: "+parms[0]
tzname = value tzname = value
elif name == "COMMENT": elif name == "COMMENT":
pass pass
else: else:
raise ValueError, "unsupported property: "+name raise ValueError("unsupported property: "+name)
else: else:
if name == "TZID": if name == "TZID":
if parms: if parms:
raise ValueError, \ raise ValueError("unsupported TZID parm: "+parms[0])
"unsupported TZID parm: "+parms[0]
tzid = value tzid = value
elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
pass pass
else: else:
raise ValueError, "unsupported property: "+name raise ValueError("unsupported property: "+name)
elif name == "BEGIN" and value == "VTIMEZONE": elif name == "BEGIN" and value == "VTIMEZONE":
tzid = None tzid = None
comps = [] comps = []
invtz = True invtz = True
def __repr__(self): def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, `self._s`) return "%s(%s)" % (self.__class__.__name__, repr(self._s))
if sys.platform != "win32": if sys.platform != "win32":
TZFILES = ["/etc/localtime", "localtime"] TZFILES = ["/etc/localtime", "localtime"]
@@ -914,7 +923,7 @@ def gettz(name=None):
for path in TZPATHS: for path in TZPATHS:
filepath = os.path.join(path, name) filepath = os.path.join(path, name)
if not os.path.isfile(filepath): if not os.path.isfile(filepath):
filepath = filepath.replace(' ','_') filepath = filepath.replace(' ', '_')
if not os.path.isfile(filepath): if not os.path.isfile(filepath):
continue continue
try: try:
+15 -16
View File
@@ -1,9 +1,8 @@
# This code was originally contributed by Jeffrey Harris. # This code was originally contributed by Jeffrey Harris.
import datetime import datetime
import struct import struct
import _winreg import winreg
__author__ = "Jeffrey Harris & Gustavo Niemeyer <gustavo@niemeyer.net>"
__all__ = ["tzwin", "tzwinlocal"] __all__ = ["tzwin", "tzwinlocal"]
@@ -15,9 +14,9 @@ TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
def _settzkeyname(): def _settzkeyname():
global TZKEYNAME global TZKEYNAME
handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
try: try:
_winreg.OpenKey(handle, TZKEYNAMENT).Close() winreg.OpenKey(handle, TZKEYNAMENT).Close()
TZKEYNAME = TZKEYNAMENT TZKEYNAME = TZKEYNAMENT
except WindowsError: except WindowsError:
TZKEYNAME = TZKEYNAME9X TZKEYNAME = TZKEYNAME9X
@@ -49,10 +48,10 @@ class tzwinbase(datetime.tzinfo):
def list(): def list():
"""Return a list of all time zones known to the system.""" """Return a list of all time zones known to the system."""
handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
tzkey = _winreg.OpenKey(handle, TZKEYNAME) tzkey = winreg.OpenKey(handle, TZKEYNAME)
result = [_winreg.EnumKey(tzkey, i) result = [winreg.EnumKey(tzkey, i)
for i in range(_winreg.QueryInfoKey(tzkey)[0])] for i in range(winreg.QueryInfoKey(tzkey)[0])]
tzkey.Close() tzkey.Close()
handle.Close() handle.Close()
return result return result
@@ -79,8 +78,8 @@ class tzwin(tzwinbase):
def __init__(self, name): def __init__(self, name):
self._name = name self._name = name
handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
tzkey = _winreg.OpenKey(handle, "%s\%s" % (TZKEYNAME, name)) tzkey = winreg.OpenKey(handle, "%s\%s" % (TZKEYNAME, name))
keydict = valuestodict(tzkey) keydict = valuestodict(tzkey)
tzkey.Close() tzkey.Close()
handle.Close() handle.Close()
@@ -118,9 +117,9 @@ class tzwinlocal(tzwinbase):
def __init__(self): def __init__(self):
handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
tzlocalkey = _winreg.OpenKey(handle, TZLOCALKEYNAME) tzlocalkey = winreg.OpenKey(handle, TZLOCALKEYNAME)
keydict = valuestodict(tzlocalkey) keydict = valuestodict(tzlocalkey)
tzlocalkey.Close() tzlocalkey.Close()
@@ -128,7 +127,7 @@ class tzwinlocal(tzwinbase):
self._dstname = keydict["DaylightName"].encode("iso-8859-1") self._dstname = keydict["DaylightName"].encode("iso-8859-1")
try: try:
tzkey = _winreg.OpenKey(handle, "%s\%s"%(TZKEYNAME, self._stdname)) tzkey = winreg.OpenKey(handle, "%s\%s"%(TZKEYNAME, self._stdname))
_keydict = valuestodict(tzkey) _keydict = valuestodict(tzkey)
self._display = _keydict["Display"] self._display = _keydict["Display"]
tzkey.Close() tzkey.Close()
@@ -165,7 +164,7 @@ def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
"""dayofweek == 0 means Sunday, whichweek 5 means last instance""" """dayofweek == 0 means Sunday, whichweek 5 means last instance"""
first = datetime.datetime(year, month, 1, hour, minute) first = datetime.datetime(year, month, 1, hour, minute)
weekdayone = first.replace(day=((dayofweek-first.isoweekday())%7+1)) weekdayone = first.replace(day=((dayofweek-first.isoweekday())%7+1))
for n in xrange(whichweek): for n in range(whichweek):
dt = weekdayone+(whichweek-n)*ONEWEEK dt = weekdayone+(whichweek-n)*ONEWEEK
if dt.month == month: if dt.month == month:
return dt return dt
@@ -173,8 +172,8 @@ def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
def valuestodict(key): def valuestodict(key):
"""Convert a registry key's values to a dictionary.""" """Convert a registry key's values to a dictionary."""
dict = {} dict = {}
size = _winreg.QueryInfoKey(key)[1] size = winreg.QueryInfoKey(key)[1]
for i in range(size): for i in range(size):
data = _winreg.EnumValue(key, i) data = winreg.EnumValue(key, i)
dict[data[0]] = data[1] dict[data[0]] = data[1]
return dict return dict
+9 -6
View File
@@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
""" """
Copyright (c) 2003-2005 Gustavo Niemeyer <gustavo@niemeyer.net> Copyright (c) 2003-2005 Gustavo Niemeyer <gustavo@niemeyer.net>
This module offers extensions to the standard python 2.3+ This module offers extensions to the standard Python
datetime module. datetime module.
""" """
from dateutil.tz import tzfile from dateutil.tz import tzfile
from tarfile import TarFile from tarfile import TarFile
import os import os
__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>" __author__ = "Tomi Pieviläinen <tomi.pievilainen@iki.fi>"
__license__ = "PSF License" __license__ = "Simplified BSD"
__all__ = ["setcachesize", "gettz", "rebuild"] __all__ = ["setcachesize", "gettz", "rebuild"]
@@ -21,8 +22,7 @@ class tzfile(tzfile):
return (gettz, (self._filename,)) return (gettz, (self._filename,))
def getzoneinfofile(): def getzoneinfofile():
filenames = os.listdir(os.path.join(os.path.dirname(__file__))) filenames = sorted(os.listdir(os.path.join(os.path.dirname(__file__))))
filenames.sort()
filenames.reverse() filenames.reverse()
for entry in filenames: for entry in filenames:
if entry.startswith("zoneinfo") and ".tar." in entry: if entry.startswith("zoneinfo") and ".tar." in entry:
@@ -66,7 +66,10 @@ def rebuild(filename, tag=None, format="gz"):
targetname = "zoneinfo%s.tar.%s" % (tag, format) targetname = "zoneinfo%s.tar.%s" % (tag, format)
try: try:
tf = TarFile.open(filename) tf = TarFile.open(filename)
for name in tf.getnames(): # The "backwards" zone file contains links to other files, so must be
# processed as last
for name in sorted(tf.getnames(),
key=lambda k: k != "backward" and k or "z"):
if not (name.endswith(".sh") or if not (name.endswith(".sh") or
name.endswith(".tab") or name.endswith(".tab") or
name == "leapseconds"): name == "leapseconds"):
Binary file not shown.
Binary file not shown.
+366
View File
@@ -0,0 +1,366 @@
"""Utilities for writing code that runs on Python 2 and 3"""
import operator
import sys
import types
__author__ = "Benjamin Peterson <benjamin@python.org>"
__version__ = "1.2.0"
# True if we are running on Python 3.
PY3 = sys.version_info[0] == 3
if PY3:
string_types = str,
integer_types = int,
class_types = type,
text_type = str
binary_type = bytes
MAXSIZE = sys.maxsize
else:
string_types = basestring,
integer_types = (int, long)
class_types = (type, types.ClassType)
text_type = unicode
binary_type = str
if sys.platform == "java":
# Jython always uses 32 bits.
MAXSIZE = int((1 << 31) - 1)
else:
# It's possible to have sizeof(long) != sizeof(Py_ssize_t).
class X(object):
def __len__(self):
return 1 << 31
try:
len(X())
except OverflowError:
# 32-bit
MAXSIZE = int((1 << 31) - 1)
else:
# 64-bit
MAXSIZE = int((1 << 63) - 1)
del X
def _add_doc(func, doc):
"""Add documentation to a function."""
func.__doc__ = doc
def _import_module(name):
"""Import module, returning the module after the last dot."""
__import__(name)
return sys.modules[name]
class _LazyDescr(object):
def __init__(self, name):
self.name = name
def __get__(self, obj, tp):
result = self._resolve()
setattr(obj, self.name, result)
# This is a bit ugly, but it avoids running this again.
delattr(tp, self.name)
return result
class MovedModule(_LazyDescr):
def __init__(self, name, old, new=None):
super(MovedModule, self).__init__(name)
if PY3:
if new is None:
new = name
self.mod = new
else:
self.mod = old
def _resolve(self):
return _import_module(self.mod)
class MovedAttribute(_LazyDescr):
def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
super(MovedAttribute, self).__init__(name)
if PY3:
if new_mod is None:
new_mod = name
self.mod = new_mod
if new_attr is None:
if old_attr is None:
new_attr = name
else:
new_attr = old_attr
self.attr = new_attr
else:
self.mod = old_mod
if old_attr is None:
old_attr = name
self.attr = old_attr
def _resolve(self):
module = _import_module(self.mod)
return getattr(module, self.attr)
class _MovedItems(types.ModuleType):
"""Lazy loading of moved objects"""
_moved_attributes = [
MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
MovedAttribute("map", "itertools", "builtins", "imap", "map"),
MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
MovedAttribute("reduce", "__builtin__", "functools"),
MovedAttribute("StringIO", "StringIO", "io"),
MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
MovedModule("builtins", "__builtin__"),
MovedModule("configparser", "ConfigParser"),
MovedModule("copyreg", "copy_reg"),
MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
MovedModule("http_cookies", "Cookie", "http.cookies"),
MovedModule("html_entities", "htmlentitydefs", "html.entities"),
MovedModule("html_parser", "HTMLParser", "html.parser"),
MovedModule("http_client", "httplib", "http.client"),
MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
MovedModule("cPickle", "cPickle", "pickle"),
MovedModule("queue", "Queue"),
MovedModule("reprlib", "repr"),
MovedModule("socketserver", "SocketServer"),
MovedModule("tkinter", "Tkinter"),
MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
MovedModule("tkinter_colorchooser", "tkColorChooser",
"tkinter.colorchooser"),
MovedModule("tkinter_commondialog", "tkCommonDialog",
"tkinter.commondialog"),
MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
MovedModule("tkinter_font", "tkFont", "tkinter.font"),
MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
"tkinter.simpledialog"),
MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
MovedModule("winreg", "_winreg"),
]
for attr in _moved_attributes:
setattr(_MovedItems, attr.name, attr)
del attr
moves = sys.modules["six.moves"] = _MovedItems("moves")
def add_move(move):
"""Add an item to six.moves."""
setattr(_MovedItems, move.name, move)
def remove_move(name):
"""Remove item from six.moves."""
try:
delattr(_MovedItems, name)
except AttributeError:
try:
del moves.__dict__[name]
except KeyError:
raise AttributeError("no such move, %r" % (name,))
if PY3:
_meth_func = "__func__"
_meth_self = "__self__"
_func_code = "__code__"
_func_defaults = "__defaults__"
_iterkeys = "keys"
_itervalues = "values"
_iteritems = "items"
else:
_meth_func = "im_func"
_meth_self = "im_self"
_func_code = "func_code"
_func_defaults = "func_defaults"
_iterkeys = "iterkeys"
_itervalues = "itervalues"
_iteritems = "iteritems"
try:
advance_iterator = next
except NameError:
def advance_iterator(it):
return it.next()
next = advance_iterator
if PY3:
def get_unbound_function(unbound):
return unbound
Iterator = object
def callable(obj):
return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
else:
def get_unbound_function(unbound):
return unbound.im_func
class Iterator(object):
def next(self):
return type(self).__next__(self)
callable = callable
_add_doc(get_unbound_function,
"""Get the function out of a possibly unbound function""")
get_method_function = operator.attrgetter(_meth_func)
get_method_self = operator.attrgetter(_meth_self)
get_function_code = operator.attrgetter(_func_code)
get_function_defaults = operator.attrgetter(_func_defaults)
def iterkeys(d):
"""Return an iterator over the keys of a dictionary."""
return iter(getattr(d, _iterkeys)())
def itervalues(d):
"""Return an iterator over the values of a dictionary."""
return iter(getattr(d, _itervalues)())
def iteritems(d):
"""Return an iterator over the (key, value) pairs of a dictionary."""
return iter(getattr(d, _iteritems)())
if PY3:
def b(s):
return s.encode("latin-1")
def u(s):
return s
if sys.version_info[1] <= 1:
def int2byte(i):
return bytes((i,))
else:
# This is about 2x faster than the implementation above on 3.2+
int2byte = operator.methodcaller("to_bytes", 1, "big")
import io
StringIO = io.StringIO
BytesIO = io.BytesIO
else:
def b(s):
return s
def u(s):
return unicode(s, "unicode_escape")
int2byte = chr
import StringIO
StringIO = BytesIO = StringIO.StringIO
_add_doc(b, """Byte literal""")
_add_doc(u, """Text literal""")
if PY3:
import builtins
exec_ = getattr(builtins, "exec")
def reraise(tp, value, tb=None):
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
print_ = getattr(builtins, "print")
del builtins
else:
def exec_(code, globs=None, locs=None):
"""Execute code in a namespace."""
if globs is None:
frame = sys._getframe(1)
globs = frame.f_globals
if locs is None:
locs = frame.f_locals
del frame
elif locs is None:
locs = globs
exec("""exec code in globs, locs""")
exec_("""def reraise(tp, value, tb=None):
raise tp, value, tb
""")
def print_(*args, **kwargs):
"""The new-style print function."""
fp = kwargs.pop("file", sys.stdout)
if fp is None:
return
def write(data):
if not isinstance(data, basestring):
data = str(data)
fp.write(data)
want_unicode = False
sep = kwargs.pop("sep", None)
if sep is not None:
if isinstance(sep, unicode):
want_unicode = True
elif not isinstance(sep, str):
raise TypeError("sep must be None or a string")
end = kwargs.pop("end", None)
if end is not None:
if isinstance(end, unicode):
want_unicode = True
elif not isinstance(end, str):
raise TypeError("end must be None or a string")
if kwargs:
raise TypeError("invalid keyword arguments to print()")
if not want_unicode:
for arg in args:
if isinstance(arg, unicode):
want_unicode = True
break
if want_unicode:
newline = unicode("\n")
space = unicode(" ")
else:
newline = "\n"
space = " "
if sep is None:
sep = space
if end is None:
end = newline
for i, arg in enumerate(args):
if i:
write(sep)
write(arg)
write(end)
_add_doc(reraise, """Reraise an exception.""")
def with_metaclass(meta, base=object):
"""Create a base class with a metaclass."""
return meta("NewBase", (base,), {})
+1 -1
View File
@@ -1,2 +1,2 @@
VERSION = '2.0.3' VERSION = '2.0.4'
BRANCH = 'desktop' BRANCH = 'desktop'