|
|
@ -2,17 +2,11 @@ from couchpotato import get_session |
|
|
|
from couchpotato.api import addApiView |
|
|
|
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent |
|
|
|
from couchpotato.core.helpers.encoding import toUnicode |
|
|
|
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \ |
|
|
|
mergeDicts |
|
|
|
from couchpotato.core.helpers.variable import splitString, tryInt |
|
|
|
from couchpotato.core.logger import CPLog |
|
|
|
from couchpotato.core.media.movie import MovieTypeBase |
|
|
|
from couchpotato.core.settings.model import Library, LibraryTitle, Media, \ |
|
|
|
Release |
|
|
|
from sqlalchemy.orm import joinedload_all |
|
|
|
from sqlalchemy.sql.expression import or_, asc, not_, desc |
|
|
|
from string import ascii_lowercase |
|
|
|
from couchpotato.core.settings.model import Media |
|
|
|
import time |
|
|
|
import os |
|
|
|
|
|
|
|
log = CPLog(__name__) |
|
|
|
|
|
|
@ -27,28 +21,6 @@ class MovieBase(MovieTypeBase): |
|
|
|
super(MovieBase, self).__init__() |
|
|
|
self.initType() |
|
|
|
|
|
|
|
addApiView('movie.list', self.listView, docs = { |
|
|
|
'desc': 'List movies in wanted list', |
|
|
|
'params': { |
|
|
|
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'}, |
|
|
|
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'}, |
|
|
|
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'}, |
|
|
|
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'}, |
|
|
|
'search': {'desc': 'Search movie title'}, |
|
|
|
}, |
|
|
|
'return': {'type': 'object', 'example': """{ |
|
|
|
'success': True, |
|
|
|
'empty': bool, any movies returned or not, |
|
|
|
'movies': array, movies found, |
|
|
|
}"""} |
|
|
|
}) |
|
|
|
addApiView('movie.get', self.getView, docs = { |
|
|
|
'desc': 'Get a movie by id', |
|
|
|
'params': { |
|
|
|
'id': {'desc': 'The id of the movie'}, |
|
|
|
} |
|
|
|
}) |
|
|
|
addApiView('movie.available_chars', self.charView) |
|
|
|
addApiView('movie.add', self.addView, docs = { |
|
|
|
'desc': 'Add new movie to the wanted list', |
|
|
|
'params': { |
|
|
@ -66,256 +38,8 @@ class MovieBase(MovieTypeBase): |
|
|
|
'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, |
|
|
|
} |
|
|
|
}) |
|
|
|
addApiView('movie.delete', self.deleteView, docs = { |
|
|
|
'desc': 'Delete a movie from the wanted list', |
|
|
|
'params': { |
|
|
|
'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'}, |
|
|
|
'delete_from': {'desc': 'Delete movie from this page', 'type': 'string: all (default), wanted, manage'}, |
|
|
|
'with_files': {'desc': 'Delete the files as well', 'type': 'bool (true or false)'}, |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
addEvent('movie.add', self.add) |
|
|
|
addEvent('movie.delete', self.delete) |
|
|
|
addEvent('movie.get', self.get) |
|
|
|
addEvent('movie.list', self.list) |
|
|
|
addEvent('movie.restatus', self.restatus) |
|
|
|
|
|
|
|
def getView(self, id = None, **kwargs): |
|
|
|
|
|
|
|
movie = self.get(id) if id else None |
|
|
|
|
|
|
|
return { |
|
|
|
'success': movie is not None, |
|
|
|
'movie': movie, |
|
|
|
} |
|
|
|
|
|
|
|
def get(self, movie_id): |
|
|
|
|
|
|
|
db = get_session() |
|
|
|
|
|
|
|
imdb_id = getImdb(str(movie_id)) |
|
|
|
|
|
|
|
if imdb_id: |
|
|
|
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first() |
|
|
|
else: |
|
|
|
m = db.query(Media).filter_by(id = movie_id).first() |
|
|
|
|
|
|
|
results = None |
|
|
|
if m: |
|
|
|
results = m.to_dict(self.default_dict) |
|
|
|
|
|
|
|
db.expire_all() |
|
|
|
return results |
|
|
|
|
|
|
|
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None): |
|
|
|
|
|
|
|
db = get_session() |
|
|
|
|
|
|
|
# Make a list from string |
|
|
|
if status and not isinstance(status, (list, tuple)): |
|
|
|
status = [status] |
|
|
|
if release_status and not isinstance(release_status, (list, tuple)): |
|
|
|
release_status = [release_status] |
|
|
|
|
|
|
|
# query movie ids |
|
|
|
q = db.query(Media) \ |
|
|
|
.with_entities(Media.id) \ |
|
|
|
.group_by(Media.id) |
|
|
|
|
|
|
|
# Filter on movie status |
|
|
|
if status and len(status) > 0: |
|
|
|
statuses = fireEvent('status.get', status, single = len(status) > 1) |
|
|
|
statuses = [s.get('id') for s in statuses] |
|
|
|
|
|
|
|
q = q.filter(Media.status_id.in_(statuses)) |
|
|
|
|
|
|
|
# Filter on release status |
|
|
|
if release_status and len(release_status) > 0: |
|
|
|
q = q.join(Media.releases) |
|
|
|
|
|
|
|
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) |
|
|
|
statuses = [s.get('id') for s in statuses] |
|
|
|
|
|
|
|
q = q.filter(Release.status_id.in_(statuses)) |
|
|
|
|
|
|
|
# Only join when searching / ordering |
|
|
|
if starts_with or search or order != 'release_order': |
|
|
|
q = q.join(Media.library, Library.titles) \ |
|
|
|
.filter(LibraryTitle.default == True) |
|
|
|
|
|
|
|
# Add search filters |
|
|
|
filter_or = [] |
|
|
|
if starts_with: |
|
|
|
starts_with = toUnicode(starts_with.lower()) |
|
|
|
if starts_with in ascii_lowercase: |
|
|
|
filter_or.append(LibraryTitle.simple_title.startswith(starts_with)) |
|
|
|
else: |
|
|
|
ignore = [] |
|
|
|
for letter in ascii_lowercase: |
|
|
|
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter))) |
|
|
|
filter_or.append(not_(or_(*ignore))) |
|
|
|
|
|
|
|
if search: |
|
|
|
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%')) |
|
|
|
|
|
|
|
if len(filter_or) > 0: |
|
|
|
q = q.filter(or_(*filter_or)) |
|
|
|
|
|
|
|
total_count = q.count() |
|
|
|
if total_count == 0: |
|
|
|
return 0, [] |
|
|
|
|
|
|
|
if order == 'release_order': |
|
|
|
q = q.order_by(desc(Release.last_edit)) |
|
|
|
else: |
|
|
|
q = q.order_by(asc(LibraryTitle.simple_title)) |
|
|
|
|
|
|
|
if limit_offset: |
|
|
|
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset |
|
|
|
limit = splt[0] |
|
|
|
offset = 0 if len(splt) is 1 else splt[1] |
|
|
|
q = q.limit(limit).offset(offset) |
|
|
|
|
|
|
|
# Get all movie_ids in sorted order |
|
|
|
movie_ids = [m.id for m in q.all()] |
|
|
|
|
|
|
|
# List release statuses |
|
|
|
releases = db.query(Release) \ |
|
|
|
.filter(Release.movie_id.in_(movie_ids)) \ |
|
|
|
.all() |
|
|
|
|
|
|
|
release_statuses = dict((m, set()) for m in movie_ids) |
|
|
|
releases_count = dict((m, 0) for m in movie_ids) |
|
|
|
for release in releases: |
|
|
|
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id)) |
|
|
|
releases_count[release.movie_id] += 1 |
|
|
|
|
|
|
|
# Get main movie data |
|
|
|
q2 = db.query(Media) \ |
|
|
|
.options(joinedload_all('library.titles')) \ |
|
|
|
.options(joinedload_all('library.files')) \ |
|
|
|
.options(joinedload_all('status')) \ |
|
|
|
.options(joinedload_all('files')) |
|
|
|
|
|
|
|
q2 = q2.filter(Media.id.in_(movie_ids)) |
|
|
|
|
|
|
|
results = q2.all() |
|
|
|
|
|
|
|
# Create dict by movie id |
|
|
|
movie_dict = {} |
|
|
|
for movie in results: |
|
|
|
movie_dict[movie.id] = movie |
|
|
|
|
|
|
|
# List movies based on movie_ids order |
|
|
|
movies = [] |
|
|
|
for movie_id in movie_ids: |
|
|
|
|
|
|
|
releases = [] |
|
|
|
for r in release_statuses.get(movie_id): |
|
|
|
x = splitString(r) |
|
|
|
releases.append({'status_id': x[0], 'quality_id': x[1]}) |
|
|
|
|
|
|
|
# Merge releases with movie dict |
|
|
|
movies.append(mergeDicts(movie_dict[movie_id].to_dict({ |
|
|
|
'library': {'titles': {}, 'files':{}}, |
|
|
|
'files': {}, |
|
|
|
}), { |
|
|
|
'releases': releases, |
|
|
|
'releases_count': releases_count.get(movie_id), |
|
|
|
})) |
|
|
|
|
|
|
|
db.expire_all() |
|
|
|
return total_count, movies |
|
|
|
|
|
|
|
def availableChars(self, status = None, release_status = None): |
|
|
|
|
|
|
|
status = status or [] |
|
|
|
release_status = release_status or [] |
|
|
|
|
|
|
|
db = get_session() |
|
|
|
|
|
|
|
# Make a list from string |
|
|
|
if not isinstance(status, (list, tuple)): |
|
|
|
status = [status] |
|
|
|
if release_status and not isinstance(release_status, (list, tuple)): |
|
|
|
release_status = [release_status] |
|
|
|
|
|
|
|
q = db.query(Media) |
|
|
|
|
|
|
|
# Filter on movie status |
|
|
|
if status and len(status) > 0: |
|
|
|
statuses = fireEvent('status.get', status, single = len(release_status) > 1) |
|
|
|
statuses = [s.get('id') for s in statuses] |
|
|
|
|
|
|
|
q = q.filter(Media.status_id.in_(statuses)) |
|
|
|
|
|
|
|
# Filter on release status |
|
|
|
if release_status and len(release_status) > 0: |
|
|
|
|
|
|
|
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1) |
|
|
|
statuses = [s.get('id') for s in statuses] |
|
|
|
|
|
|
|
q = q.join(Media.releases) \ |
|
|
|
.filter(Release.status_id.in_(statuses)) |
|
|
|
|
|
|
|
q = q.join(Library, LibraryTitle) \ |
|
|
|
.with_entities(LibraryTitle.simple_title) \ |
|
|
|
.filter(LibraryTitle.default == True) |
|
|
|
|
|
|
|
titles = q.all() |
|
|
|
|
|
|
|
chars = set() |
|
|
|
for title in titles: |
|
|
|
try: |
|
|
|
char = title[0][0] |
|
|
|
char = char if char in ascii_lowercase else '#' |
|
|
|
chars.add(str(char)) |
|
|
|
except: |
|
|
|
log.error('Failed getting title for %s', title.libraries_id) |
|
|
|
|
|
|
|
if len(chars) == 25: |
|
|
|
break |
|
|
|
|
|
|
|
db.expire_all() |
|
|
|
return ''.join(sorted(chars)) |
|
|
|
|
|
|
|
def listView(self, **kwargs): |
|
|
|
|
|
|
|
status = splitString(kwargs.get('status')) |
|
|
|
release_status = splitString(kwargs.get('release_status')) |
|
|
|
limit_offset = kwargs.get('limit_offset') |
|
|
|
starts_with = kwargs.get('starts_with') |
|
|
|
search = kwargs.get('search') |
|
|
|
order = kwargs.get('order') |
|
|
|
|
|
|
|
total_movies, movies = self.list( |
|
|
|
status = status, |
|
|
|
release_status = release_status, |
|
|
|
limit_offset = limit_offset, |
|
|
|
starts_with = starts_with, |
|
|
|
search = search, |
|
|
|
order = order |
|
|
|
) |
|
|
|
|
|
|
|
return { |
|
|
|
'success': True, |
|
|
|
'empty': len(movies) == 0, |
|
|
|
'total': total_movies, |
|
|
|
'movies': movies, |
|
|
|
} |
|
|
|
|
|
|
|
def charView(self, **kwargs): |
|
|
|
|
|
|
|
status = splitString(kwargs.get('status', None)) |
|
|
|
release_status = splitString(kwargs.get('release_status', None)) |
|
|
|
chars = self.availableChars(status, release_status) |
|
|
|
|
|
|
|
return { |
|
|
|
'success': True, |
|
|
|
'empty': len(chars) == 0, |
|
|
|
'chars': chars, |
|
|
|
} |
|
|
|
|
|
|
|
def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None): |
|
|
|
if not params: params = {} |
|
|
@ -451,145 +175,10 @@ class MovieBase(MovieTypeBase): |
|
|
|
|
|
|
|
fireEvent('media.restatus', m.id) |
|
|
|
|
|
|
|
movie_dict = m.to_dict(self.default_dict) |
|
|
|
movie_dict = m.to_dict(self.search_dict) |
|
|
|
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id)) |
|
|
|
|
|
|
|
db.expire_all() |
|
|
|
return { |
|
|
|
'success': True, |
|
|
|
} |
|
|
|
|
|
|
|
def deleteView(self, id = '', **kwargs): |
|
|
|
|
|
|
|
ids = splitString(id) |
|
|
|
for movie_id in ids: |
|
|
|
self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'), with_files = kwargs.get('with_files')) |
|
|
|
|
|
|
|
return { |
|
|
|
'success': True, |
|
|
|
} |
|
|
|
|
|
|
|
def deleteFiles(self, instance): |
|
|
|
directories = dict() |
|
|
|
|
|
|
|
# Walk through all files in the Couch database |
|
|
|
for file_ in instance.files: |
|
|
|
# Add the directories and filename prefixes to a list so we can |
|
|
|
# remove the directories and related files as well |
|
|
|
directory = os.path.dirname(file_.path) |
|
|
|
if directory not in directories: |
|
|
|
directories[directory] = set() |
|
|
|
directories[directory].add(os.path.splitext(file_.path)[0]) |
|
|
|
|
|
|
|
if os.path.isfile(file_.path): |
|
|
|
try: |
|
|
|
os.remove(file_.path) |
|
|
|
log.info('Removed %s', file_.path) |
|
|
|
except: |
|
|
|
log.error('Unable to remove %s', file_.path) |
|
|
|
|
|
|
|
# Walk through the directories and file prefixes for removal if |
|
|
|
# possible |
|
|
|
for directory, prefixes in directories.iteritems(): |
|
|
|
if os.path.isdir(directory): |
|
|
|
|
|
|
|
# If the files in the directory have the same name as the |
|
|
|
# expected files (except for extensions and stuff), remove them |
|
|
|
files = os.listdir(directory) |
|
|
|
for file_ in files: |
|
|
|
for prefix in prefixes: |
|
|
|
if file_.startswith(prefix): |
|
|
|
try: |
|
|
|
os.remove(file_) |
|
|
|
print 'rmoeving', file_ |
|
|
|
log.info('Removed %s', file_) |
|
|
|
except: |
|
|
|
log.error('Unable to remove %s', file_) |
|
|
|
|
|
|
|
try: |
|
|
|
os.rmdir(directory) |
|
|
|
log.info('Removed %s', directory) |
|
|
|
except: |
|
|
|
log.error('Unable to remove %s', directory) |
|
|
|
|
|
|
|
def delete(self, movie_id, delete_from = None, with_files = False): |
|
|
|
|
|
|
|
db = get_session() |
|
|
|
|
|
|
|
movie = db.query(Media).filter_by(id = movie_id).first() |
|
|
|
if movie: |
|
|
|
if with_files: |
|
|
|
self.deleteFiles(movie) |
|
|
|
|
|
|
|
deleted = False |
|
|
|
if delete_from == 'all': |
|
|
|
db.delete(movie) |
|
|
|
db.commit() |
|
|
|
deleted = True |
|
|
|
else: |
|
|
|
done_status = fireEvent('status.get', 'done', single = True) |
|
|
|
|
|
|
|
total_releases = len(movie.releases) |
|
|
|
total_deleted = 0 |
|
|
|
new_movie_status = None |
|
|
|
for release in movie.releases: |
|
|
|
if with_files: |
|
|
|
self.deleteFiles(release) |
|
|
|
|
|
|
|
if delete_from in ['wanted', 'snatched', 'late']: |
|
|
|
if release.status_id != done_status.get('id'): |
|
|
|
db.delete(release) |
|
|
|
total_deleted += 1 |
|
|
|
new_movie_status = 'done' |
|
|
|
elif delete_from == 'manage': |
|
|
|
if release.status_id == done_status.get('id'): |
|
|
|
db.delete(release) |
|
|
|
total_deleted += 1 |
|
|
|
new_movie_status = 'active' |
|
|
|
db.commit() |
|
|
|
|
|
|
|
if total_releases == total_deleted: |
|
|
|
db.delete(movie) |
|
|
|
db.commit() |
|
|
|
deleted = True |
|
|
|
elif new_movie_status: |
|
|
|
new_status = fireEvent('status.get', new_movie_status, single = True) |
|
|
|
movie.profile_id = None |
|
|
|
movie.status_id = new_status.get('id') |
|
|
|
db.commit() |
|
|
|
else: |
|
|
|
fireEvent('movie.restatus', movie.id, single = True) |
|
|
|
|
|
|
|
if deleted: |
|
|
|
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict()) |
|
|
|
|
|
|
|
db.expire_all() |
|
|
|
return True |
|
|
|
|
|
|
|
def restatus(self, movie_id): |
|
|
|
|
|
|
|
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) |
|
|
|
|
|
|
|
db = get_session() |
|
|
|
|
|
|
|
m = db.query(Media).filter_by(id = movie_id).first() |
|
|
|
if not m or len(m.library.titles) == 0: |
|
|
|
log.debug('Can\'t restatus movie, doesn\'t seem to exist.') |
|
|
|
return False |
|
|
|
|
|
|
|
log.debug('Changing status for %s', m.library.titles[0].title) |
|
|
|
if not m.profile: |
|
|
|
m.status_id = done_status.get('id') |
|
|
|
else: |
|
|
|
move_to_wanted = True |
|
|
|
|
|
|
|
for t in m.profile.types: |
|
|
|
for release in m.releases: |
|
|
|
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish): |
|
|
|
move_to_wanted = False |
|
|
|
|
|
|
|
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id') |
|
|
|
|
|
|
|
db.commit() |
|
|
|
|
|
|
|
return True |
|
|
|