From cb5fc694abc175d2cc7f3a6ca08a94d04a48135e Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 11 Sep 2011 00:58:26 +0200 Subject: [PATCH] Downloading of single releases and more --- .../core/downloaders/blackhole/main.py | 14 ++- couchpotato/core/downloaders/sabnzbd/main.py | 19 ++-- couchpotato/core/plugins/base.py | 23 +++-- .../core/plugins/movie/static/movie.css | 81 ++++++++++++++-- .../core/plugins/movie/static/movie.js | 84 +++++++++++++++-- .../core/plugins/movie/static/search.js | 4 +- couchpotato/core/plugins/quality/main.py | 2 +- couchpotato/core/plugins/release/main.py | 55 +++++++++-- couchpotato/core/plugins/renamer/main.py | 47 ++++++++-- couchpotato/core/plugins/scanner/main.py | 16 ++-- couchpotato/core/plugins/searcher/main.py | 63 +++++++------ couchpotato/core/providers/base.py | 20 +++- .../core/providers/nzb/newzbin/main.py | 10 +- .../core/providers/nzb/newznab/main.py | 14 +++ .../core/providers/nzb/nzbindex/main.py | 4 +- .../core/providers/nzb/nzbmatrix/main.py | 2 + couchpotato/core/providers/nzb/nzbs/main.py | 2 + couchpotato/core/settings/model.py | 3 + couchpotato/static/images/edit.png | Bin 1015 -> 0 bytes .../images/{check.png => icon.check.png} | Bin .../images/{delete.png => icon.delete.png} | Bin couchpotato/static/images/icon.download.png | Bin 0 -> 502 bytes couchpotato/static/images/icon.edit.png | Bin 0 -> 624 bytes .../static/images/{imdb.png => icon.imdb.png} | Bin .../images/{rating.png => icon.rating.png} | Bin couchpotato/static/images/icon.refresh.png | Bin 0 -> 674 bytes couchpotato/static/images/reload.png | Bin 726 -> 0 bytes couchpotato/static/scripts/page/wanted.js | 4 +- couchpotato/static/style/main.css | 12 ++- couchpotato/static/style/page/settings.css | 2 +- libs/multipartpost.py | 88 ++++++++++++++++++ 31 files changed, 457 insertions(+), 112 deletions(-) delete mode 100644 couchpotato/static/images/edit.png rename couchpotato/static/images/{check.png => icon.check.png} (100%) rename couchpotato/static/images/{delete.png => icon.delete.png} (100%) create mode 100644 couchpotato/static/images/icon.download.png create mode 100644 couchpotato/static/images/icon.edit.png rename couchpotato/static/images/{imdb.png => icon.imdb.png} (100%) rename couchpotato/static/images/{rating.png => icon.rating.png} (100%) create mode 100644 couchpotato/static/images/icon.refresh.png delete mode 100644 couchpotato/static/images/reload.png create mode 100644 libs/multipartpost.py diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py index a0d142bd..f59a70b6 100644 --- a/couchpotato/core/downloaders/blackhole/main.py +++ b/couchpotato/core/downloaders/blackhole/main.py @@ -27,18 +27,16 @@ class Blackhole(Downloader): try: if not os.path.isfile(fullPath): log.info('Downloading %s to %s.' % (data.get('type'), fullPath)) - if isfunction(data.get('download')): - file = data.get('download')() - else: - file = self.urlopen(data.get('url')) - if not file or file == '': + try: + file = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) + + with open(fullPath, 'wb') as f: + f.write(file) + except: log.debug('Failed download file: %s' % data.get('name')) return False - with open(fullPath, 'wb') as f: - f.write(file) - return True else: log.info('File %s already exists.' % fullPath) diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index e49522fb..8d56a8fa 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -6,6 +6,7 @@ from urllib import urlencode import base64 import os import re +import traceback log = CPLog(__name__) @@ -37,15 +38,11 @@ class Sabnzbd(Downloader): params = { 'apikey': self.conf('api_key'), 'cat': self.conf('category'), - 'mode': 'addurl', - 'name': data.get('url'), + 'mode': 'addfile', 'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)), } - # sabNzbd complains about "invalid archive file" for newzbin urls - # added using addurl, works fine with addid - if data.get('addbyid'): - params['mode'] = 'addid' + nzb_file = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) if pp: params['script'] = pp_script_fn @@ -53,9 +50,9 @@ class Sabnzbd(Downloader): url = cleanHost(self.conf('host')) + "api?" + urlencode(params) try: - data = self.urlopen(url) - except Exception, e: - log.error("Unable to connect to SAB: %s" % e) + data = self.urlopen(url, params = {"nzbfile": (params['nzbname'] + ".nzb", nzb_file)}, multipart = True) + except Exception: + log.error("Unable to connect to SAB: %s" % traceback.format_exc()) return False result = data.strip() @@ -63,7 +60,7 @@ class Sabnzbd(Downloader): log.error("SABnzbd didn't return anything.") return False - log.debug("Result text from SAB: " + result) + log.debug("Result text from SAB: " + result[:40]) if result == "ok": log.info("NZB sent to SAB successfully.") return True @@ -71,7 +68,7 @@ class Sabnzbd(Downloader): log.error("Incorrect username/password.") return False else: - log.error("Unknown error: " + result) + log.error("Unknown error: " + result[:40]) return False def buildPp(self, imdb_id): diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index e844a53f..6e342154 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -4,7 +4,9 @@ from couchpotato.core.helpers.variable import getExt from couchpotato.core.logger import CPLog from couchpotato.environment import Env from flask.helpers import send_from_directory +from libs.multipartpost import MultipartPostHandler from urlparse import urlparse +import cookielib import glob import math import os.path @@ -80,7 +82,7 @@ class Plugin(object): return False # http request - def urlopen(self, url, timeout = 10, params = {}, headers = {}): + def urlopen(self, url, timeout = 10, params = {}, headers = {}, multipart = False): socket.setdefaulttimeout(timeout) @@ -88,15 +90,24 @@ class Plugin(object): self.wait(host) try: - log.info('Opening url: %s, params: %s' % (url, params)) - data = urllib.urlencode(params) if len(params) > 0 else None - request = urllib2.Request(url, data, headers) + if multipart: + log.info('Opening multipart url: %s, params: %s' % (url, params.iterkeys())) + request = urllib2.Request(url, params, headers) - data = urllib2.urlopen(request).read() + cookies = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler) + + data = opener.open(request).read() + else: + log.info('Opening url: %s, params: %s' % (url, params)) + data = urllib.urlencode(params) if len(params) > 0 else None + request = urllib2.Request(url, data, headers) + + data = urllib2.urlopen(request).read() except IOError, e: log.error('Failed opening url, %s: %s' % (url, e)) - data = None + raise self.http_last_use[host] = time.time() diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index d7088690..ff5fd6a9 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -1,4 +1,7 @@ -/* @override http://localhost:5000/static/movie_plugin/movie.css */ +/* @override + http://localhost:5000/static/movie_plugin/movie.css + http://192.168.1.20:5000/static/movie_plugin/movie.css +*/ .movies { padding: 20px 0; @@ -79,13 +82,21 @@ float: left; width: 5%; padding: 0 0 0 3%; - background: url('../images/rating.png') no-repeat left center; } .movies .info .description { clear: both; width: 95%; } + + .movies .data .quality span { + padding: 5px; + font-weight: bold; + } + + .movies .data .quality .available { color: orange; } + .movies .data .quality .snatched { color: lightgreen; } + .movies .data .actions { position: absolute; right: 15px; @@ -96,17 +107,14 @@ .movies .data:hover .action:hover { opacity: 1; } .movies .data .action { - background: no-repeat center; + background-repeat: no-repeat; + background-position: center; display: inline-block; width: 20px; height: 20px; padding: 3px; opacity: 0; } - .movies .data .action.refresh { background-image: url('../images/reload.png'); } - .movies .data .action.delete { background-image: url('../images/delete.png'); } - .movies .data .action.edit { background-image: url('../images/edit.png'); } - .movies .data .action.imdb { background-image: url('../images/imdb.png'); } .movies .delete_container { clear: both; @@ -142,6 +150,65 @@ padding: 2%; } + .movies .options .releases { + height: 157px; + overflow: auto; + margin: -20px -20px -20px 110px; + padding: 15px 0 5px; + } + .movies .options .releases .item { + border-bottom: 1px solid rgba(255,255,255,0.1); + } + .movies .options .releases .item:last-child { border: 0; } + .movies .options .releases .item:nth-child(even) { + background: rgba(255,255,255,0.05); + } + .movies .options .releases .item:not(.head):hover { + background: rgba(255,255,255,0.03); + } + + .movies .options .releases .item > * { + display: inline-block; + padding: 0 5px; + width: 50px; + min-height: 24px; + white-space: nowrap; + text-overflow: ellipsis; + -moz-text-overflow: ellipsis; + text-align: center; + vertical-align: top; + border-left: 1px solid rgba(255, 255, 255, 0.1); + } + .movies .options .releases .item > *:first-child { + border: 0; + } + .movies .options .releases .provider { + width: 120px; + } + .movies .options .releases .name { + width: 420px; + overflow: hidden; + text-align: left; + padding: 0 10px; + } + + .movies .options .releases a { + width: 16px !important; + height: 16px; + opacity: 0.8; + } + .movies .options .releases a:hover { + opacity: 1; + } + + .movies .options .releases .head > * { + font-weight: bold; + font-size: 14px; + padding-top: 4px; + padding-bottom: 4px; + height: auto; + } + .movies .alph_nav ul { list-style: none; padding: 0; diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index 2d244168..5b6bf2df 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -32,7 +32,7 @@ var Movie = new Class({ self.year = new Element('div.year', { 'text': self.data.library.year || 'Unknown' }), - self.rating = new Element('div.rating', { + self.rating = new Element('div.rating.icon', { 'text': self.data.library.rating }), self.description = new Element('div.description', { @@ -47,11 +47,18 @@ var Movie = new Class({ ); self.profile.get('types').each(function(type){ + + // Check if quality is snatched + var is_snatched = self.data.releases.filter(function(release){ + return release.quality_id == type.quality_id && release.status.identifier == 'snatched' + }).pick(); + var q = Quality.getQuality(type.quality_id); new Element('span', { - 'text': ' '+q.label + 'text': q.label, + 'class': is_snatched ? 'snatched' : '' }).inject(self.quality); - }) + }); Object.each(self.options.actions, function(action, key){ self.actions.adopt( @@ -127,7 +134,7 @@ var Movie = new Class({ var MovieAction = new Class({ - class_name: 'action', + class_name: 'action icon', initialize: function(movie){ var self = this; @@ -193,7 +200,7 @@ var ReleaseAction = new Class({ self.id = self.movie.get('identifier'); - self.el = new Element('a.releases', { + self.el = new Element('a.releases.icon.download', { 'title': 'Show the releases that are available for ' + self.movie.getTitle(), 'events': { 'click': self.show.bind(self) @@ -211,16 +218,77 @@ var ReleaseAction = new Class({ $(self.movie.thumbnail).clone(), self.release_container = new Element('div.releases') ).inject(self.movie, 'top'); + + // Header + new Element('div.item.head').adopt( + new Element('span.name', {'text': 'Release name'}), + new Element('span.quality', {'text': 'Quality'}), + new Element('span.age', {'text': 'Age'}), + new Element('span.score', {'text': 'Score'}), + new Element('span.provider', {'text': 'Provider'}) + ).inject(self.release_container) Array.each(self.movie.data.releases, function(release){ - p(release); new Element('div', { - 'text': release.title - }).inject(self.release_container) + 'class': 'item ' + release.status.identifier + }).adopt( + new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}), + new Element('span.quality', {'text': release.quality.label}), + new Element('span.age', {'text': self.get(release, 'age')}), + new Element('span.score', {'text': self.get(release, 'score')}), + new Element('span.provider', {'text': self.get(release, 'provider')}), + new Element('a.download.icon', { + 'events': { + 'click': function(e){ + (e).stop(); + self.download(release); + } + } + }), + new Element('a.delete.icon', { + 'events': { + 'click': function(e){ + (e).stop(); + self.delete(release); + } + } + }) + ).inject(self.release_container) }); } self.movie.slide('in'); }, + + download: function(release){ + var self = this; + + p(release) + + Api.request('release.download', { + 'data': { + 'id': release.id + } + }); + }, + + delete: function(release){ + var self = this; + + Api.request('release.delete', { + 'data': { + 'id': release.id + } + }) + + }, + + get: function(release, type){ + var self = this; + + return (release.info.filter(function(info){ + return type == info.identifier + }).pick() || {}).value + } }); \ No newline at end of file diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js index 5e66d30d..67483115 100644 --- a/couchpotato/core/plugins/movie/static/search.js +++ b/couchpotato/core/plugins/movie/static/search.js @@ -29,7 +29,7 @@ Block.Search = new Class({ }).adopt( new Element('div.pointer'), self.results = new Element('div.results') - ).fade('hide') + ).hide() ); self.spinner = new Spinner(self.result_container); @@ -52,7 +52,7 @@ Block.Search = new Class({ if(self.hidden == bool) return; - self.result_container.fade(bool ? 0 : 1) + self.result_container[bool ? 'hide' : 'show'](); if(bool){ History.removeEvent('change', self.hideResults.bind(self, !bool)); diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 6676d9f9..d01dc365 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -13,7 +13,7 @@ log = CPLog(__name__) class QualityPlugin(Plugin): qualities = [ - {'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': []}, + {'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate']}, {'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']}, {'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']}, {'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']}, diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 96f7a3d2..0749364f 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -1,8 +1,10 @@ from couchpotato import get_session +from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent +from couchpotato.core.helpers.request import getParam, jsonified from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import File, Release, Movie +from couchpotato.core.settings.model import File, Release as Relea, Movie from sqlalchemy.sql.expression import and_, or_ log = CPLog(__name__) @@ -13,6 +15,8 @@ class Release(Plugin): def __init__(self): addEvent('release.add', self.add) + addApiView('release.download', self.download) + def add(self, group): db = get_session() @@ -30,22 +34,22 @@ class Release(Plugin): db.add(movie) db.commit() - # Add release + # Add Release snatched_status = fireEvent('status.get', 'snatched', single = True) - release = db.query(Release).filter( + rel = db.query(Relea).filter( or_( - Release.identifier == identifier, - and_(Release.identifier.startswith(group['library']['identifier'], Release.status_id == snatched_status.get('id'))) + Relea.identifier == identifier, + and_(Relea.identifier.startswith(group['library']['identifier'], Relea.status_id == snatched_status.get('id'))) ) ).first() - if not release: - release = Release( + if not rel: + rel = Relea( identifier = identifier, movie = movie, quality_id = group['meta_data']['quality'].get('id'), status_id = done_status.get('id') ) - db.add(release) + db.add(rel) db.commit() # Add each file type @@ -54,10 +58,10 @@ class Release(Plugin): added_file = self.saveFile(file, type = type, include_media_info = type is 'movie') try: added_file = db.query(File).filter_by(id = added_file.get('id')).one() - release.files.append(added_file) + Relea.files.append(added_file) db.commit() except Exception, e: - log.debug('Failed to attach "%s" to release: %s' % (file, e)) + log.debug('Failed to attach "%s" to Relea: %s' % (file, e)) db.remove() @@ -73,3 +77,34 @@ class Release(Plugin): # Check database and update/insert if necessary return fireEvent('file.add', path = file, part = self.getPartNumber(file), type = self.file_types[type], properties = properties, single = True) + def download(self): + + db = get_session() + id = getParam('id') + + rel = db.query(Relea).filter_by(id = id).first() + if rel: + item = {} + for info in rel.info: + item[info.identifier] = info.value + + # Get matching provider + provider = fireEvent('provider.belongs_to', item['url'], single = True) + item['download'] = provider.download + + fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({ + 'profile': {'types': {'quality': {}}}, + 'releases': {'status': {}, 'quality': {}}, + 'library': {'titles': {}, 'files':{}}, + 'files': {} + })) + + return jsonified({ + 'success': True + }) + else: + log.error('Couldn\'t find release with id: %s' % id) + + return jsonified({ + 'success': False + }) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index a3c1859e..2715a853 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -4,7 +4,7 @@ from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import getExt from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Library +from couchpotato.core.settings.model import Library, Movie import os.path import re import shutil @@ -134,8 +134,18 @@ class Renamer(Plugin): # Move DVD files (no structure renaming) if group['is_dvd'] and file_type is 'movie': - structure_dir = file.split(group['dirname'])[-1].lstrip(os.path.sep) - rename_files[file] = os.path.join(destination, final_folder_name, structure_dir) + found = False + for top_dir in ['video_ts', 'audio_ts', 'bdmv', 'certificate']: + has_string = file.lower().find(os.path.sep + top_dir + os.path.sep) + if has_string >= 0: + structure_dir = file[has_string:].lstrip(os.path.sep) + rename_files[file] = os.path.join(destination, final_folder_name, structure_dir) + found = True + break + + if not found: + log.error('Could not determin dvd structure for: %s' % file) + # Do rename others else: rename_files[file] = os.path.join(destination, final_folder_name, final_file_name) @@ -162,15 +172,41 @@ class Renamer(Plugin): # Before renaming, remove the lower quality files db = get_session() + library = db.query(Library).filter_by(identifier = group['library']['identifier']).first() done_status = fireEvent('status.get', 'done', single = True) + active_status = fireEvent('status.get', 'active', single = True) + for movie in library.movies: + + # Mark movie "done" onces it found the quality with the finish check + try: + if movie.status_id == active_status.get('id'): + for type in movie.profile.types: + if type.quality_id == group['meta_data']['quality']['id'] and type.finish: + movie.status_id = done_status.get('id') + db.commit() + except Exception, e: + log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc())) + + # Go over current movie releases for release in movie.releases: + + # This is where CP removes older, lesser quality releases if release.quality.order > group['meta_data']['quality']['order']: log.info('Removing older release for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label)) + + for file in release.files: + log.info('Removing (not really) "%s"' % file.path) + + # When a release already exists elif release.status_id is done_status.get('id'): + + # Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc if release.quality.order is group['meta_data']['quality']['order']: log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label)) + + # Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan else: log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label)) @@ -190,10 +226,7 @@ class Renamer(Plugin): break - for file in release.files: - log.info('Removing (not really) "%s"' % file.path) - - # Rename + # Rename all files marked for src in rename_files: if rename_files[src]: diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 537f7b96..d4b82450 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -23,11 +23,11 @@ class Scanner(Plugin): 'trailer': 1048576, # 1MB } ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files - ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts'] + ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate'] extensions = { 'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img'], 'dvd': ['vts_*', 'vob'], - 'nfo': ['nfo', 'txt', 'tag'], + 'nfo': ['nfo', 'nfo-orig', 'txt', 'tag'], 'subtitle': ['sub', 'srt', 'ssa', 'ass'], 'subtitle_extra': ['idx'], 'trailer': ['mov', 'mp4', 'flv'] @@ -210,13 +210,13 @@ class Scanner(Plugin): group['parentdir'] = os.path.dirname(movie_file) group['dirname'] = None - folders = group['parentdir'].replace(folder, '').split(os.path.sep) - folders.reverse() + folder_names = group['parentdir'].replace(folder, '').split(os.path.sep) + folder_names.reverse() # Try and get a proper dirname, so no "A", "Movie", "Download" etc - for folder in folders: - if folder.lower() not in self.ignore_names and len(folder) > 2: - group['dirname'] = folder + for folder_name in folder_names: + if folder_name.lower() not in self.ignore_names and len(folder_name) > 2: + group['dirname'] = folder_name break break @@ -426,7 +426,7 @@ class Scanner(Plugin): if list(set(file.lower().split(os.path.sep)) & set(['video_ts', 'audio_ts'])): return True - for needle in ['vts_', 'video_ts', 'audio_ts']: + for needle in ['vts_', 'video_ts', 'audio_ts', 'bdmv', 'certificate']: if needle in file.lower(): return True diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 2f1e9930..877e56e4 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -7,6 +7,7 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Movie, Release, ReleaseInfo from couchpotato.environment import Env import re +import traceback log = CPLog(__name__) @@ -17,6 +18,7 @@ class Searcher(Plugin): addEvent('searcher.all', self.all) addEvent('searcher.single', self.single) addEvent('searcher.correct_movie', self.correctMovie) + addEvent('searcher.download', self.download) # Schedule cronjob fireEvent('schedule.cron', 'searcher.all', self.all, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) @@ -34,7 +36,7 @@ class Searcher(Plugin): for movie in movies: - self.single(movie.to_dict(deep = { + self.single(movie.to_dict({ 'profile': {'types': {'quality': {}}}, 'releases': {'status': {}, 'quality': {}}, 'library': {'titles': {}, 'files':{}}, @@ -47,11 +49,8 @@ class Searcher(Plugin): def single(self, movie): - downloaded_status = fireEvent('status.get', 'downloaded', single = True) available_status = fireEvent('status.get', 'available', single = True) - snatched_status = fireEvent('status.get', 'snatched', single = True) - successful = False for type in movie['profile']['types']: has_better_quality = 0 @@ -85,37 +84,19 @@ class Searcher(Plugin): db.commit() for info in nzb: - rls_info = ReleaseInfo( - identifier = info, - value = nzb[info] - ) - rls.info.append(rls_info) + try: + rls_info = ReleaseInfo( + identifier = info, + value = nzb[info] + ) + rls.info.append(rls_info) + except Exception: + log.debug('Couldn\'t add %s to ReleaseInfo: %s' % (info, traceback.format_exc())) db.commit() for nzb in sorted_results: - successful = fireEvent('download', data = nzb, movie = movie, single = True) - - if successful: - - # Mark release as snatched - db = get_session() - rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first() - rls.status_id = snatched_status.get('id') - db.commit() - - # Mark movie snatched if quality is finish-checked - if type['finish']: - mvie = db.query(Movie).filter_by(id = movie['id']).first() - mvie.status_id = snatched_status.get('id') - db.commit() - - log.info('Downloading of %s successful.' % nzb.get('name')) - fireEvent('movie.snatched', message = 'Downloading of %s successful.' % nzb.get('name'), data = rls.to_dict()) - - return True - - return False + return self.download(data = nzb, movie = movie) else: log.info('Better quality (%s) already available or snatched for %s' % (type['quality']['label'], default_title)) break @@ -126,6 +107,26 @@ class Searcher(Plugin): return False + def download(self, data, movie): + + snatched_status = fireEvent('status.get', 'snatched', single = True) + + successful = fireEvent('download', data = data, movie = movie, single = True) + + if successful: + + # Mark release as snatched + db = get_session() + rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() + rls.status_id = snatched_status.get('id') + db.commit() + + log.info('Downloading of %s successful.' % data.get('name')) + fireEvent('movie.snatched', message = 'Downloading of %s successful.' % data.get('name'), data = rls.to_dict()) + + return True + + return False def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs): diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index df27865e..7e08440c 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -2,7 +2,6 @@ from couchpotato.core.event import addEvent from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env -from urllib2 import URLError from urlparse import urlparse import re import time @@ -60,6 +59,20 @@ class YarrProvider(Provider): sizeMb = ['mb', 'mib'] sizeKb = ['kb', 'kib'] + def __init__(self): + addEvent('provider.belongs_to', self.belongsTo) + + def belongsTo(self, url, host = None): + try: + hostname = urlparse(url).hostname + download_url = host if host else self.urls['download'] + if hostname in download_url: + return self + except: + log.debug('Url % s doesn\'t belong to %s' % (url, self.getName())) + + return + def parseSize(self, size): sizeRaw = size.lower() @@ -96,11 +109,16 @@ class NZBProvider(YarrProvider): type = 'nzb' def __init__(self): + super(NZBProvider, self).__init__() + addEvent('provider.nzb.search', self.search) addEvent('provider.yarr.search', self.search) addEvent('provider.nzb.feed', self.feed) + def download(self, url = '', nzb_id = ''): + return self.urlopen(url) + def feed(self): return [] diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py index 8933be9c..e7e157a4 100644 --- a/couchpotato/core/providers/nzb/newzbin/main.py +++ b/couchpotato/core/providers/nzb/newzbin/main.py @@ -13,10 +13,9 @@ log = CPLog(__name__) class Newzbin(NZBProvider, RSS): urls = { - 'search': 'https://www.newzbin.com/search/', 'download': 'http://www.newzbin.com/api/dnzb/', + 'search': 'https://www.newzbin.com/search/', } - searchUrl = 'https://www.newzbin.com/search/' format_ids = { 2: ['scr'], @@ -36,7 +35,7 @@ class Newzbin(NZBProvider, RSS): def search(self, movie, quality): results = [] - if self.isDisabled() or not self.isAvailable(self.searchUrl): + if self.isDisabled() or not self.isAvailable(self.urls['search']): return results format_id = self.getFormatId(type) @@ -97,11 +96,12 @@ class Newzbin(NZBProvider, RSS): new = { 'id': id, 'type': 'nzb', + 'provider': self.getName(), 'name': title, 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'size': self.parseSize(size), 'url': str(self.getTextElement(nzb, '{%s}nzb' % REPORT_NS)), - 'download': lambda: self.download(id), + 'download': self.download, 'detail_url': str(self.getTextElement(nzb, 'link')), 'description': self.getTextElement(nzb, "description"), 'check_nzb': False, @@ -121,7 +121,7 @@ class Newzbin(NZBProvider, RSS): return results - def download(self, nzb_id): + def download(self, url = '', nzb_id = ''): try: log.info('Download nzb from newzbin, report id: %s ' % nzb_id) diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index 17ac3832..028162eb 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -5,6 +5,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.providers.base import NZBProvider from dateutil.parser import parse from urllib import urlencode +from urlparse import urlparse import time import xml.etree.ElementTree as XMLTree @@ -130,11 +131,13 @@ class Newznab(NZBProvider, RSS): id = self.getTextElement(nzb, "guid").split('/')[-1:].pop() new = { 'id': id, + 'provider': self.getName(), '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': (self.getUrl(host['host'], self.urls['detail']) % id) + self.getApiExt(host), 'content': self.getTextElement(nzb, "description"), } @@ -173,6 +176,17 @@ class Newznab(NZBProvider, RSS): return list + def belongsTo(self, url): + + hosts = self.getHosts() + + for host in hosts: + result = super(Newznab, self).belongsTo(url, host = host['host']) + if result: + return result + + return + def getUrl(self, host, type): return cleanHost(host) + 'api?t=' + type diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py index a1b377c7..f44e586a 100644 --- a/couchpotato/core/providers/nzb/nzbindex/main.py +++ b/couchpotato/core/providers/nzb/nzbindex/main.py @@ -14,7 +14,7 @@ log = CPLog(__name__) class NzbIndex(NZBProvider, RSS): urls = { - 'download': 'http://www.nzbindex.nl/download/%s/%s', + 'download': 'http://www.nzbindex.nl/download/', 'api': 'http://www.nzbindex.nl/rss/', #http://www.nzbindex.nl/rss/?q=due+date+720p&age=1000&sort=agedesc&minsize=3500&maxsize=10000 } @@ -63,10 +63,12 @@ class NzbIndex(NZBProvider, RSS): new = { 'id': id, 'type': 'nzb', + 'provider': self.getName(), 'name': self.getTextElement(nzb, "title"), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'size': enclosure['length'], 'url': enclosure['url'], + 'download': self.download, 'detail_url': enclosure['url'].replace('/download/', '/release/'), 'description': self.getTextElement(nzb, "description"), 'check_nzb': True, diff --git a/couchpotato/core/providers/nzb/nzbmatrix/main.py b/couchpotato/core/providers/nzb/nzbmatrix/main.py index 11049159..da602adc 100644 --- a/couchpotato/core/providers/nzb/nzbmatrix/main.py +++ b/couchpotato/core/providers/nzb/nzbmatrix/main.py @@ -81,10 +81,12 @@ class NZBMatrix(NZBProvider, RSS): new = { 'id': id, 'type': 'nzb', + 'provider': self.getName(), 'name': title, 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'size': self.parseSize(size), 'url': self.urls['download'] % id + self.getApiExt(), + 'download': self.download, 'detail_url': self.urls['detail'] % id, 'description': self.getTextElement(nzb, "description"), 'check_nzb': True, diff --git a/couchpotato/core/providers/nzb/nzbs/main.py b/couchpotato/core/providers/nzb/nzbs/main.py index 86489329..a86f9d76 100644 --- a/couchpotato/core/providers/nzb/nzbs/main.py +++ b/couchpotato/core/providers/nzb/nzbs/main.py @@ -71,10 +71,12 @@ class Nzbs(NZBProvider, RSS): new = { 'id': id, 'type': 'nzb', + 'provider': self.getName(), 'name': self.getTextElement(nzb, "title"), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'size': self.parseSize(self.getTextElement(nzb, "description").split('
')[1].split('">')[1]), 'url': self.urls['download'] % (id, self.getApiExt()), + 'download': self.download, 'detail_url': self.urls['detail'] % id, 'description': self.getTextElement(nzb, "description"), 'check_nzb': True, diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 4b96bfe6..b2efd061 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -47,6 +47,9 @@ class Library(Entity): files = ManyToMany('File') info = OneToMany('LibraryInfo') + def title(self): + return self.titles[0]['title'] + class LibraryInfo(Entity): """""" diff --git a/couchpotato/static/images/edit.png b/couchpotato/static/images/edit.png deleted file mode 100644 index 531925289ad7e0b6e4841dd3c2abfc6a6bd5470e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1015 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2v2cW`+Qt5Lck$|NsBrz4z$qwX6O8eP7ReeK}(I?e@Hnb9vq# zDD019THhUWx`^xAB!4Lpfz@-Td`c32w0Zia75z6RbzHj2**B418y z_|QuWBn&f(_XIqwRe^ljK(;?B{&JdNkp2fq}SoGnNrpabv>)xKZ zz;NFqN2b6#?etbZaX+T5?`BT^AXT{K>ZJ_}wtxMiS@Gw4%sJfy3`a!7Z_jA`Ed&Zd MPgg&ebxsLQ03%|X#{d8T diff --git a/couchpotato/static/images/check.png b/couchpotato/static/images/icon.check.png similarity index 100% rename from couchpotato/static/images/check.png rename to couchpotato/static/images/icon.check.png diff --git a/couchpotato/static/images/delete.png b/couchpotato/static/images/icon.delete.png similarity index 100% rename from couchpotato/static/images/delete.png rename to couchpotato/static/images/icon.delete.png diff --git a/couchpotato/static/images/icon.download.png b/couchpotato/static/images/icon.download.png new file mode 100644 index 0000000000000000000000000000000000000000..ca3d04345ce100e1c6ad64a51e1dfaf131b96b84 GIT binary patch literal 502 zcmVz@;j(q!3lK=n!AY({UO#lFTB>(_`g8%^e{{R4h=>PzA zFaQARU;qF*m;eA5Z<1fdMgRZ-Y)M2xRCwB?lRroTQ5?s=@3`#qEUsG(=iCj(WSK^1JfJgk#>Kc-mgWkyXzvXAN=6`9>3q` zz2Eo!9wQ?B$K>^bF$Mro^;`o$IU=`td2~|&0RN$KL}K}L{~YfN!`s-B;E;exb|BsuV7VIt{Oa>1$A@crNqouIIRv2T z7gO@9UrN`IP318WV_2^3`9zG7P32Ka*ARYU4`9zkOB>W7L5Ag+Fzqk591{{`XoEWJ znP^1<8U?*-jCi*{Y$Ks0!CVQJZGyQH5=s*NVH?JXcN+!0>Ibm(RIMgaDQcOk_)hHj zVDAtw{0WKJF76(CFh;z4UAjF1Fb|AB5h3&+l%kgDsCtebrae;i92K=p2Y?c;A)-h? s01_?^1Hvr?AS<7*JOKEn!C&oj0Idkr!fJw>?*IS*07*qoM6N<$f{gmfVgLXD literal 0 HcmV?d00001 diff --git a/couchpotato/static/images/icon.edit.png b/couchpotato/static/images/icon.edit.png new file mode 100644 index 0000000000000000000000000000000000000000..19ff8bd2f46944bf4113e6bb1a72f8ed44194c88 GIT binary patch literal 624 zcmV-$0+0QPP)#DY*E7-SSw3n2)CECP#eitNH$Er=q53Pk3G ziQ&c2ToUHm67434D00MXlUBHmW^QxNHTUgw91=lu+Y1i|-oyFv!a0Y*MoqNt5YpxC zN_LAdk(Hf3@0{_Ygkm-htHp|K!RBlC1xsJ&aPEa0ZIMsA+MTm=B>|HsOy2EUWv#NM z9n(WY7~b80<;Wr&PY0Ny!0g$&u4ppTT~%3uHE{TLy$>;-V8mua2OgvOLAoHqjJK*P z%Q;y{vsI3(F%?p)JUSmeL}TJ~>7*$fQFtC5KYK^PWtqdtcODY6h^#Mb^yYE$4} zq{bpgW;sqLVWHTm$NY02?+J?*0v?g!%IkQx;1x}EvP406Xj>%6{7xZD7|4|-;&@q4 z;$PfnE{;V;i$0V$H(8c5r#tc9wCFE4rIa-!1q-0q!CnXT4+IPh@KIlp_)|DJ;of^@6O&$Qb|%0Qj}Sozsbo8Q|>Mbn=U zP2E7#wB?O`Ep!GZny!_X7GPuz^XAJUAIBgXg^BNwVUi{61V^rojQRaE_qNj4NLdoVFk+|dMdJ*; zhruC&vq5gXzRrV0P}bF)f!c~)+<0;`kvy2BK~0+`3U)UG7hM{IuyBhaR6s z^E4C0gyZR%rDXZ6T~Z#euCIvb`VE|rHoo@?;#gG;J%Lwr*M*c6rP9`TeALEboY+=M z_9iFvJX#TP`AWOxTOW5H#;`tQNBIm9*+5DUdmdObp`;j!>}EcEvauMR>P~t_*#n}! zx{k1Mn;#1yWC@t0h{~O4*tM2I<{~!k28MaoAd1baJIiXe5uVjC=3!%jS3*P_5Qj*e zMuH)F`}<^aT#vEUo#>M7I&k@5J6}Uhj82Wy|5@Hbx@RKrQvzahya-(6+;Z6|hDkxh z+p$h@q{FG4u2oi&6#bWJbgAj{v=B}^Y5y2!b=uZ+`M(4h0H#Ie$NWKnJpcdz07*qo IM6N<$g2$IKwEzGB literal 0 HcmV?d00001 diff --git a/couchpotato/static/images/reload.png b/couchpotato/static/images/reload.png deleted file mode 100644 index 031f2fd2036de78ea708d7f7f803f23d3784714d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 726 zcmV;{0xA88P)5r0007=Nkl$^ z-Afa26o!8o1>JOCQ4oDl;7!p@Q3TC8x*v(5kD>0+RkyEs?x;W_W&Jx2gyc?A}Hbd4FgX1GdP+eOk_ z(@}MX*Me!r zfX1mrQWMLyYB_q7j=bkNBx#Q$H%rJJiQ;VM9!}zNW-Lw|%gaWBf zg7$;@AG@X9d1PAmku?-f^!;$mmYyX*fiagtYbKZ4Pjf~aL%cEyN!c;n6V645e_@U^ zT?{8*Ze@SjChjWB2`u=6;6sm4rFp%nC?? z<_L+pb>s~RDjb4)-vk$GBe4xxgAM8W6R5_MDd<0jvZErTi;Si-+^~kB8W+^N1xd=? zyfGSr;(haNN^FrRoDs+j`*_e-!t%>(8EQag*urDCK;;nBeF-B)y`2{?nwBP{K8>Yn zY&Qz$4vGdhkn(aTnfe1<(r@FIbu~>M!Nbv2#8>P^diN+1r5jN{s$By3{If_k(WG~* zr{?=w>Rf_`2|=?*&@v?`wyYts{16I7HO}v2cxGI@>o>35we@W?$;yMAQ|%&EyP1@l zaFWUoBQMRMp;3cp%FT*j2=$TUuCEN*KG56Oh5gHD$aDbl7oy&;B8l}XtpET307*qo IM6N<$f;9_P{r~^~ diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index da98232a..b1a60c6b 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -27,7 +27,7 @@ var MovieActions = {}; MovieActions.Wanted = { 'IMBD': IMDBAction - //,'releases': ReleaseAction + ,'releases': ReleaseAction ,'Edit': new Class({ @@ -207,7 +207,6 @@ MovieActions.Wanted = { self.chain( function(){ - $(movie).mask().addClass('loading'); self.callChain(); }, function(){ @@ -236,6 +235,5 @@ MovieActions.Wanted = { MovieActions.Snatched = { 'IMBD': IMDBAction - ,'Releases': ReleaseAction ,'Delete': MovieActions.Wanted.Delete }; \ No newline at end of file diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index e0d9c380..293c5688 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -137,10 +137,18 @@ form { } /*** Icons ***/ -.icon.delete { - background: url('../images/delete.png') no-repeat; +.icon { display: inline-block; + background: center no-repeat; } +.icon.delete { background-image: url('../images/icon.delete.png'); } +.icon.download { background-image: url('../images/icon.download.png'); } +.icon.edit { background-image: url('../images/icon.edit.png'); } +.icon.check { background-image: url('../images/icon.check.png'); } +.icon.folder { background-image: url('../images/icon.folder.png'); } +.icon.imdb { background-image: url('../images/icon.imdb.png'); } +.icon.refresh { background-image: url('../images/icon.refresh.png'); } +.icon.rating { background-image: url('../images/icon.rating.png'); } /*** Navigation ***/ .header { diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index 2ce8954b..ae93a2f6 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -103,7 +103,7 @@ border: 0; } .page.settings .ctrlHolder.save_success:not(:first-child) { - background: url('../../images/check.png') no-repeat 7px center; + background: url('../../images/icon.check.png') no-repeat 7px center; } .page.settings .ctrlHolder:last-child { border: none; } .page.settings .ctrlHolder:hover { background-color: rgba(255,255,255,0.05); } diff --git a/libs/multipartpost.py b/libs/multipartpost.py new file mode 100644 index 00000000..38dfbd12 --- /dev/null +++ b/libs/multipartpost.py @@ -0,0 +1,88 @@ +#!/usr/bin/python + +#### +# 06/2010 Nic Wolfe +# 02/2006 Will Holcomb +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# + +import urllib +import urllib2 +import mimetools, mimetypes +import os, sys + +# Controls how sequences are uncoded. If true, elements may be given multiple values by +# assigning a sequence. +doseq = 1 + +class MultipartPostHandler(urllib2.BaseHandler): + handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first + + def http_request(self, request): + data = request.get_data() + if data is not None and type(data) != str: + v_files = [] + v_vars = [] + try: + for(key, value) in data.items(): + if type(value) in (file, list, tuple): + v_files.append((key, value)) + else: + v_vars.append((key, value)) + except TypeError: + systype, value, traceback = sys.exc_info() + raise TypeError, "not a valid non-string sequence or mapping object", traceback + + if len(v_files) == 0: + data = urllib.urlencode(v_vars, doseq) + else: + boundary, data = MultipartPostHandler.multipart_encode(v_vars, v_files) + contenttype = 'multipart/form-data; boundary=%s' % boundary + if(request.has_header('Content-Type') + and request.get_header('Content-Type').find('multipart/form-data') != 0): + print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data') + request.add_unredirected_header('Content-Type', contenttype) + + request.add_data(data) + return request + + @staticmethod + def multipart_encode(vars, files, boundary = None, buffer = None): + if boundary is None: + boundary = mimetools.choose_boundary() + if buffer is None: + buffer = '' + for(key, value) in vars: + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"' % key + buffer += '\r\n\r\n' + value + '\r\n' + for(key, fd) in files: + + # allow them to pass in a file or a tuple with name & data + if type(fd) == file: + name_in = fd.name + fd.seek(0) + data_in = fd.read() + elif type(fd) in (tuple, list): + name_in, data_in = fd + + filename = os.path.basename(name_in) + contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename) + buffer += 'Content-Type: %s\r\n' % contenttype + # buffer += 'Content-Length: %s\r\n' % file_size + buffer += '\r\n' + data_in + '\r\n' + buffer += '--%s--\r\n\r\n' % boundary + return boundary, buffer + + https_request = http_request