You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
505 lines
17 KiB
505 lines
17 KiB
import uuid
|
|
import datetime
|
|
|
|
from sqlalchemy import Column, ForeignKey, Table, Index
|
|
from sqlalchemy.ext.associationproxy import AssociationProxy
|
|
from sqlalchemy.ext.hybrid import hybrid_property
|
|
from sqlalchemy.orm import relationship, object_mapper, ColumnProperty, class_mapper
|
|
from sqlalchemy.orm.exc import UnmappedInstanceError
|
|
from sqlalchemy.orm.query import Query
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
from couchpotato.core.helpers.encoding import toUnicode
|
|
from sqlalchemy.ext.mutable import Mutable
|
|
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, String, \
|
|
TypeDecorator
|
|
import json
|
|
import time
|
|
|
|
|
|
class SetEncoder(json.JSONEncoder):
|
|
def default(self, obj):
|
|
if isinstance(obj, set):
|
|
return list(obj)
|
|
return json.JSONEncoder.default(self, obj)
|
|
|
|
|
|
class JsonType(TypeDecorator):
|
|
impl = UnicodeText
|
|
|
|
def process_bind_param(self, value, dialect):
|
|
try:
|
|
return toUnicode(json.dumps(value, cls = SetEncoder))
|
|
except:
|
|
try:
|
|
return toUnicode(json.dumps(value, cls = SetEncoder, encoding = 'latin-1'))
|
|
except:
|
|
raise
|
|
|
|
def process_result_value(self, value, dialect):
|
|
return json.loads(value if value else '{}')
|
|
|
|
|
|
class MutableDict(Mutable, dict):
|
|
|
|
@classmethod
|
|
def coerce(cls, key, value):
|
|
if not isinstance(value, MutableDict):
|
|
if isinstance(value, dict):
|
|
return MutableDict(value)
|
|
return Mutable.coerce(key, value)
|
|
else:
|
|
return value
|
|
|
|
def __delitem(self, key):
|
|
dict.__delitem__(self, key)
|
|
self.changed()
|
|
|
|
def __setitem__(self, key, value):
|
|
dict.__setitem__(self, key, value)
|
|
self.changed()
|
|
|
|
def __getstate__(self):
|
|
return dict(self)
|
|
|
|
def __setstate__(self, state):
|
|
self.update(self)
|
|
|
|
def update(self, *args, **kwargs):
|
|
super(MutableDict, self).update(*args, **kwargs)
|
|
self.changed()
|
|
|
|
MutableDict.associate_with(JsonType)
|
|
|
|
Base = declarative_base()
|
|
|
|
COLUMN_BLACKLIST = ('_sa_polymorphic_on', )
|
|
|
|
def is_mapped_class(cls):
|
|
try:
|
|
class_mapper(cls)
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
def is_like_list(instance, relation):
|
|
"""Returns ``True`` if and only if the relation of `instance` whose name is
|
|
`relation` is list-like.
|
|
|
|
A relation may be like a list if, for example, it is a non-lazy one-to-many
|
|
relation, or it is a dynamically loaded one-to-many.
|
|
|
|
"""
|
|
if relation in instance._sa_class_manager:
|
|
return instance._sa_class_manager[relation].property.uselist
|
|
related_value = getattr(type(instance), relation, None)
|
|
return isinstance(related_value, AssociationProxy)
|
|
|
|
class TableHelper():
|
|
def to_dict(self, deep = None, exclude = None, include = None,
|
|
exclude_relations = None, include_relations = None,
|
|
include_methods = None):
|
|
instance = self
|
|
|
|
if (exclude is not None or exclude_relations is not None) and \
|
|
(include is not None or include_relations is not None):
|
|
raise ValueError('Cannot specify both include and exclude.')
|
|
# create a list of names of columns, including hybrid properties
|
|
try:
|
|
columns = [p.key for p in object_mapper(instance).iterate_properties
|
|
if isinstance(p, ColumnProperty)]
|
|
except UnmappedInstanceError:
|
|
return instance
|
|
for parent in type(instance).mro():
|
|
columns += [key for key, value in parent.__dict__.items()
|
|
if isinstance(value, hybrid_property)]
|
|
# filter the columns based on exclude and include values
|
|
if exclude is not None:
|
|
columns = (c for c in columns if c not in exclude)
|
|
elif include is not None:
|
|
columns = (c for c in columns if c in include)
|
|
# create a dictionary mapping column name to value
|
|
result = dict((col, getattr(instance, col)) for col in columns
|
|
if not (col.startswith('__') or col in COLUMN_BLACKLIST))
|
|
# add any included methods
|
|
if include_methods is not None:
|
|
result.update(dict((method, getattr(instance, method)()) for method in include_methods if not '.' in method))
|
|
# Check for objects in the dictionary that may not be serializable by
|
|
# default. Specifically, convert datetime and date objects to ISO 8601
|
|
# format, and convert UUID objects to hexadecimal strings.
|
|
for key, value in result.items():
|
|
# TODO We can get rid of this when issue #33 is resolved.
|
|
if isinstance(value, datetime.date):
|
|
result[key] = value.isoformat()
|
|
elif isinstance(value, uuid.UUID):
|
|
result[key] = str(value)
|
|
elif is_mapped_class(type(value)):
|
|
result[key] = value.to_dict()
|
|
# recursively call _to_dict on each of the `deep` relations
|
|
deep = deep or {}
|
|
for relation, rdeep in deep.items():
|
|
# Get the related value so we can see if it is None, a list, a query
|
|
# (as specified by a dynamic relationship loader), or an actual
|
|
# instance of a model.
|
|
relatedvalue = getattr(instance, relation)
|
|
if relatedvalue is None:
|
|
result[relation] = None
|
|
continue
|
|
# Determine the included and excluded fields for the related model.
|
|
newexclude = None
|
|
newinclude = None
|
|
if exclude_relations is not None and relation in exclude_relations:
|
|
newexclude = exclude_relations[relation]
|
|
elif (include_relations is not None and
|
|
relation in include_relations):
|
|
newinclude = include_relations[relation]
|
|
# Determine the included methods for the related model.
|
|
newmethods = None
|
|
if include_methods is not None:
|
|
newmethods = [method.split('.', 1)[1] for method in include_methods
|
|
if method.split('.', 1)[0] == relation]
|
|
if is_like_list(instance, relation):
|
|
result[relation] = [inst.to_dict(rdeep, exclude = newexclude,
|
|
include = newinclude,
|
|
include_methods = newmethods)
|
|
for inst in relatedvalue]
|
|
continue
|
|
# If the related value is dynamically loaded, resolve the query to get
|
|
# the single instance.
|
|
if isinstance(relatedvalue, Query):
|
|
relatedvalue = relatedvalue.one()
|
|
result[relation] = relatedvalue.to_dict(rdeep, exclude = newexclude,
|
|
include = newinclude,
|
|
include_methods = newmethods)
|
|
|
|
return result
|
|
|
|
|
|
movie_files = Table('movie_files__file_movie', Base.metadata,
|
|
Column('movie_id', Integer, ForeignKey('movie.id'), nullable = False),
|
|
Column('file_id', Integer, ForeignKey('file.id'), nullable = False),
|
|
Index('movie_files_idx', 'movie_id', 'file_id', unique = True)
|
|
)
|
|
|
|
release_files = Table('release_files__file_release', Base.metadata,
|
|
Column('release_id', Integer, ForeignKey('release.id'), nullable = False),
|
|
Column('file_id', Integer, ForeignKey('file.id'), nullable = False),
|
|
Index('release_files_idx', 'release_id', 'file_id', unique = True)
|
|
)
|
|
|
|
library_files = Table('library_files__file_library', Base.metadata,
|
|
Column('library_id', Integer, ForeignKey('library.id'), nullable = False),
|
|
Column('file_id', Integer, ForeignKey('file.id'), nullable = False),
|
|
Index('library_files_idx', 'library_id', 'file_id', unique = True)
|
|
)
|
|
|
|
class Movie(Base, TableHelper):
|
|
__tablename__ = 'movie'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
"""Movie Resource a movie could have multiple releases
|
|
The files belonging to the movie object are global for the whole movie
|
|
such as trailers, nfo, thumbnails"""
|
|
|
|
last_edit = Column(Integer, default = lambda: int(time.time()), index = True)
|
|
type = 'movie' # Compat tv branch
|
|
|
|
library_id = Column(Integer, ForeignKey('library.id'), index = True)
|
|
status_id = Column(Integer, ForeignKey('status.id'), index = True)
|
|
profile_id = Column(Integer, ForeignKey('profile.id'), index = True)
|
|
category_id = Column(Integer, ForeignKey('category.id'), index = True)
|
|
|
|
library = relationship('Library') #cascade = 'delete, delete-orphan', single_parent = True)
|
|
status = relationship('Status')
|
|
profile = relationship('Profile')
|
|
category = relationship('Category')
|
|
releases = relationship('Release') #, cascade = 'all, delete-orphan')
|
|
files = relationship('File', secondary = movie_files) #, cascade = 'all, delete-orphan', single_parent = True)
|
|
|
|
Media = Movie # Compat tv branch
|
|
|
|
|
|
class Library(Base, TableHelper):
|
|
__tablename__ = 'library'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
""""""
|
|
|
|
year = Column(Integer)
|
|
identifier = Column(String(20), index = True)
|
|
|
|
plot = Column(UnicodeText)
|
|
tagline = Column(UnicodeText(255))
|
|
info = Column(JsonType)
|
|
|
|
status_id = Column(Integer, ForeignKey('status.id'), index = True)
|
|
status = relationship('Status')
|
|
|
|
movies = relationship('Movie') #, cascade = 'all, delete-orphan')
|
|
titles = relationship('LibraryTitle', order_by="desc(LibraryTitle.default)") #, cascade = 'all, delete-orphan')
|
|
files = relationship('File', secondary = library_files) #, cascade = 'all, delete-orphan', single_parent = True)
|
|
|
|
|
|
class LibraryTitle(Base, TableHelper):
|
|
__tablename__ = 'librarytitle'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
""""""
|
|
|
|
#using_options(order_by = '-default')
|
|
|
|
title = Column(Unicode)
|
|
simple_title = Column(Unicode, index = True)
|
|
default = Column(Boolean, default = False, index = True)
|
|
|
|
language = relationship('Language')
|
|
|
|
libraries_id = Column(Integer, ForeignKey('library.id'), index = True)
|
|
libraries = relationship('Library')
|
|
|
|
|
|
class Language(Base, TableHelper):
|
|
__tablename__ = 'language'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
""""""
|
|
|
|
identifier = Column(String(20), index = True)
|
|
label = Column(Unicode)
|
|
|
|
titles_id = Column(Integer, ForeignKey('librarytitle.id'), index = True)
|
|
titles = relationship('LibraryTitle')
|
|
|
|
|
|
class Release(Base, TableHelper):
|
|
__tablename__ = 'release'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
"""Logically groups all files that belong to a certain release, such as
|
|
parts of a movie, subtitles."""
|
|
|
|
last_edit = Column(Integer, default = lambda: int(time.time()), index = True)
|
|
identifier = Column(String(100), index = True)
|
|
|
|
movie_id = Column(Integer, ForeignKey('movie.id'), index = True)
|
|
movie = relationship('Movie')
|
|
|
|
status_id = Column(Integer, ForeignKey('status.id'), index = True)
|
|
status = relationship('Status')
|
|
|
|
quality_id = Column(Integer, ForeignKey('quality.id'), index = True)
|
|
quality = relationship('Quality')
|
|
|
|
files = relationship('File', secondary = release_files)
|
|
info = relationship('ReleaseInfo') #, cascade = 'all, delete-orphan')
|
|
|
|
def to_dict(self, deep = None, exclude = None, **kwargs):
|
|
if not exclude: exclude = []
|
|
if not deep: deep = {}
|
|
|
|
orig_dict = super(Release, self).to_dict(deep = deep, exclude = exclude)
|
|
|
|
new_info = {}
|
|
for info in orig_dict.get('info', []):
|
|
|
|
value = info['value']
|
|
try: value = int(info['value'])
|
|
except: pass
|
|
|
|
new_info[info['identifier']] = value
|
|
|
|
orig_dict['info'] = new_info
|
|
|
|
return orig_dict
|
|
|
|
|
|
class ReleaseInfo(Base, TableHelper):
|
|
__tablename__ = 'releaseinfo'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
"""Properties that can be bound to a file for off-line usage"""
|
|
|
|
identifier = Column(String(50), index = True)
|
|
value = Column(Unicode(255), nullable = False)
|
|
|
|
release_id = Column(Integer, ForeignKey('release.id'), index = True)
|
|
release = relationship('Release')
|
|
|
|
|
|
class Status(Base, TableHelper):
|
|
__tablename__ = 'status'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
"""The status of a release, such as Downloaded, Deleted, Wanted etc"""
|
|
|
|
identifier = Column(String(20), unique = True)
|
|
label = Column(Unicode(20))
|
|
|
|
releases = relationship('Release')
|
|
movies = relationship('Movie')
|
|
|
|
|
|
class Quality(Base, TableHelper):
|
|
__tablename__ = 'quality'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
"""Quality name of a release, DVD, 720p, DVD-Rip etc"""
|
|
|
|
#using_options(order_by = 'order')
|
|
|
|
identifier = Column(String(20), unique = True)
|
|
label = Column(Unicode(20))
|
|
order = Column(Integer, default = 0, index = True)
|
|
|
|
size_min = Column(Integer)
|
|
size_max = Column(Integer)
|
|
|
|
releases = relationship('Release')
|
|
profile_types = relationship('ProfileType', order_by="asc(ProfileType.order)")
|
|
|
|
|
|
class Profile(Base, TableHelper):
|
|
__tablename__ = 'profile'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
""""""
|
|
|
|
#using_options(order_by = 'order')
|
|
|
|
label = Column(Unicode(50))
|
|
order = Column(Integer, default = 0, index = True)
|
|
core = Column(Boolean, default = False)
|
|
hide = Column(Boolean, default = False)
|
|
|
|
movie = relationship('Movie')
|
|
types = relationship('ProfileType', order_by="asc(ProfileType.order)") #, cascade = 'all, delete-orphan')
|
|
|
|
def to_dict(self, deep = None, exclude = None, **kwargs):
|
|
if not exclude: exclude = []
|
|
if not deep: deep = {}
|
|
|
|
orig_dict = super(Profile, self).to_dict(deep = deep, exclude = exclude)
|
|
orig_dict['core'] = orig_dict.get('core') or False
|
|
orig_dict['hide'] = orig_dict.get('hide') or False
|
|
|
|
return orig_dict
|
|
|
|
|
|
class Category(Base, TableHelper):
|
|
__tablename__ = 'category'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
""""""
|
|
|
|
#using_options(order_by = 'order')
|
|
|
|
label = Column(Unicode(50))
|
|
order = Column(Integer, default = 0, index = True)
|
|
required = Column(Unicode(255))
|
|
preferred = Column(Unicode(255))
|
|
ignored = Column(Unicode(255))
|
|
destination = Column(Unicode(255))
|
|
|
|
movie = relationship('Movie')
|
|
|
|
|
|
class ProfileType(Base, TableHelper):
|
|
__tablename__ = 'profiletype'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
""""""
|
|
|
|
#using_options(order_by = 'order')
|
|
|
|
order = Column(Integer, default = 0, index = True)
|
|
finish = Column(Boolean, default = True)
|
|
wait_for = Column(Integer, default = 0)
|
|
|
|
quality_id = Column(Integer, ForeignKey('quality.id'), index = True)
|
|
quality = relationship('Quality')
|
|
|
|
profile_id = Column(Integer, ForeignKey('profile.id'), index = True)
|
|
profile = relationship('Profile')
|
|
|
|
|
|
class File(Base, TableHelper):
|
|
__tablename__ = 'file'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
"""File that belongs to a release."""
|
|
|
|
path = Column(Unicode(255), nullable = False, unique = True)
|
|
part = Column(Integer, default = 1)
|
|
available = Column(Boolean, default = True)
|
|
|
|
type_id = Column(Integer, ForeignKey('filetype.id'), index = True)
|
|
type = relationship('FileType')
|
|
|
|
properties = relationship('FileProperty')
|
|
|
|
movie = relationship('Movie', secondary = movie_files)
|
|
release = relationship('Release', secondary = release_files)
|
|
library = relationship('Library', secondary = library_files)
|
|
|
|
|
|
class FileType(Base, TableHelper):
|
|
__tablename__ = 'filetype'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
"""Types could be trailer, subtitle, movie, partial movie etc."""
|
|
|
|
identifier = Column(String(20), unique = True)
|
|
type = Column(Unicode(20))
|
|
name = Column(Unicode(50), nullable = False)
|
|
|
|
files = relationship('File')
|
|
|
|
|
|
class FileProperty(Base, TableHelper):
|
|
__tablename__ = 'fileproperty'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
"""Properties that can be bound to a file for off-line usage"""
|
|
|
|
identifier = Column(String(20), index = True)
|
|
value = Column(Unicode(255), nullable = False)
|
|
|
|
file_id = Column(Integer, ForeignKey('file.id'), index = True)
|
|
file = relationship('File')
|
|
|
|
|
|
class Notification(Base, TableHelper):
|
|
__tablename__ = 'notification'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
""""""
|
|
|
|
#using_options(order_by = 'added')
|
|
|
|
added = Column(Integer, default = lambda: int(time.time()), index = True)
|
|
read = Column(Boolean, default = False, index = True)
|
|
message = Column(Unicode(255))
|
|
data = Column(JsonType)
|
|
|
|
|
|
class Properties(Base, TableHelper):
|
|
__tablename__ = 'properties'
|
|
id = Column(Integer, primary_key = True)
|
|
|
|
""""""
|
|
|
|
identifier = Column(String(50), index = True)
|
|
value = Column(Unicode(255), nullable = False)
|
|
|
|
|
|
def setup():
|
|
"""Setup the database and create the tables that don't exists yet"""
|
|
from couchpotato.environment import Env
|
|
|
|
engine = Env.getEngine()
|
|
Base.metadata.create_all(engine)
|
|
|
|
try:
|
|
engine.execute("PRAGMA journal_mode = WAL")
|
|
engine.execute("PRAGMA temp_store = MEMORY")
|
|
except:
|
|
pass
|
|
|