|
|
|
|
@ -13,129 +13,156 @@ import sqlalchemy.orm
|
|
|
|
|
|
|
|
|
|
# TODO: l o g g i n g
|
|
|
|
|
HASH_CHUNK_SIZE = 10485760 # 10mb
|
|
|
|
|
_db_session_maker = sqlalchemy.orm.sessionmaker()
|
|
|
|
|
_engine = None
|
|
|
|
|
_configured = False
|
|
|
|
|
|
|
|
|
|
# TODO: Support DAT credit, DAT filenames, and checking DAT completeness.
|
|
|
|
|
#DatData = collections.namedtuple('DatData', 'UUID, name, website, version, image_list')
|
|
|
|
|
_SQLBase = sqlalchemy.ext.declarative.declarative_base()
|
|
|
|
|
|
|
|
|
|
class MetadataDBSessionException(Exception):
|
|
|
|
|
'''This exception is raised when something goes wrong with a database session.'''
|
|
|
|
|
'''
|
|
|
|
|
Metadata ORM classes for SQLAlchemy. For a detailed description of each piece of data, refer to
|
|
|
|
|
metadata/README.md
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
class Platform(_SQLBase):
|
|
|
|
|
'''SQLAlchemy ORM class for platform metadata.'''
|
|
|
|
|
__tablename__ = 'platforms'
|
|
|
|
|
id = sqlalchemy.Column(
|
|
|
|
|
sqlalchemy.Integer, sqlalchemy.Sequence('platform_id_sequence'), primary_key=True
|
|
|
|
|
)
|
|
|
|
|
fullname = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
|
|
|
|
shortcode = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False)
|
|
|
|
|
regional_names = sqlalchemy.Column(sqlalchemy.String)
|
|
|
|
|
|
|
|
|
|
def _uuidgen():
|
|
|
|
|
return str(uuid.uuid4())
|
|
|
|
|
release_groups = sqlalchemy.orm.relationship(
|
|
|
|
|
'ReleaseGroup', order_by=ReleaseGroup.id, back_populates='platform'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_SQLBase = sqlalchemy.ext.declarative.declarative_base()
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return 'Platform: id: %s, fullname: %s, shortcode: %s' % (
|
|
|
|
|
self.id, self.fullname, self.shortcode
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
class Release(_SQLBase):
|
|
|
|
|
'''SQLAlchemy ORM class for ROM image metadata.'''
|
|
|
|
|
__tablename__ = 'images'
|
|
|
|
|
id = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.Sequence('image_id_sequence'),
|
|
|
|
|
primary_key=True)
|
|
|
|
|
uuid = sqlalchemy.Column(sqlalchemy.String, nullable=False, default=_uuidgen)
|
|
|
|
|
sha1sum = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False)
|
|
|
|
|
format = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
|
|
|
|
region = sqlalchemy.Column(sqlalchemy.String)
|
|
|
|
|
version = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
|
|
|
|
disambiguation = sqlalchemy.Column(sqlalchemy.String)
|
|
|
|
|
release_group_id = sqlalchemy.Column(sqlalchemy.Integer,
|
|
|
|
|
sqlalchemy.ForeignKey('release_groups.id'))
|
|
|
|
|
|
|
|
|
|
release_group = sqlalchemy.orm.relationship('ReleaseGroup', back_populates='releases')
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return ('ROM Image: id: %s, uuid: %s, sha1sum: %s, release-group: %s, region: %s, '
|
|
|
|
|
'version: %s, disambiguation: %s' % (
|
|
|
|
|
self.id, self.uuid, self.sha1sum, self.release_group.name, self.region,
|
|
|
|
|
self.version, self.disambiguation))
|
|
|
|
|
|
|
|
|
|
class ReleaseGroup(_SQLBase):
|
|
|
|
|
'''SQLAlchemy ORM class for release group metadata.'''
|
|
|
|
|
__tablename__ = 'release_groups'
|
|
|
|
|
id = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.Sequence('image_id_sequence'),
|
|
|
|
|
primary_key=True)
|
|
|
|
|
uuid = sqlalchemy.Column(sqlalchemy.String, nullable=False, default=_uuidgen)
|
|
|
|
|
name = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False)
|
|
|
|
|
platform_id = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.ForeignKey('platforms.id'))
|
|
|
|
|
|
|
|
|
|
platform = sqlalchemy.orm.relationship('Platform', back_populates='release_groups')
|
|
|
|
|
images = sqlalchemy.orm.relationship('Release', back_populates='release_group')
|
|
|
|
|
'''SQLAlchemy ORM class for release group metadata.'''
|
|
|
|
|
__tablename__ = 'release_groups'
|
|
|
|
|
id = sqlalchemy.Column(
|
|
|
|
|
sqlalchemy.Integer, sqlalchemy.Sequence('release_group_id_sequence'), primary_key=True
|
|
|
|
|
)
|
|
|
|
|
name = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False)
|
|
|
|
|
platform_id = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.ForeignKey('platforms.id'))
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return 'Release Group: id: %s, uuid: %s, name: %s, platform:%s' % (self.id, self.uuid,
|
|
|
|
|
self.name, self.platform.fullname)
|
|
|
|
|
releases = sqlalchemy.orm.relationship('Release', back_populates='release_group')
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return 'Release Group: id: %s, name: %s, platform:%s' % (
|
|
|
|
|
self.id, self.name, self.platform.fullname
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
class Platform(_SQLBase):
|
|
|
|
|
'''SQLAlchemy ORM class for platform metadata.'''
|
|
|
|
|
__tablename__ = 'platforms'
|
|
|
|
|
id = sqlalchemy.Column(sqlalchemy.Integer, sqlalchemy.Sequence('platform_id_sequence'),
|
|
|
|
|
primary_key=True)
|
|
|
|
|
uuid = sqlalchemy.Column(sqlalchemy.String, nullable=False, default=_uuidgen)
|
|
|
|
|
fullname = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
|
|
|
|
shortcode = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False)
|
|
|
|
|
|
|
|
|
|
release_groups = sqlalchemy.orm.relationship('ReleaseGroup', order_by=ReleaseGroup.id,
|
|
|
|
|
back_populates='platform')
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return 'Platform: id: %s, uuid: %s, fullname: %s, shortcode: %s' % (self.id, self.uuid,
|
|
|
|
|
self.fullname, self.shortcode)
|
|
|
|
|
|
|
|
|
|
def configure(db_path):
|
|
|
|
|
'''
|
|
|
|
|
Configure and initialize the database for the entire module.
|
|
|
|
|
Currently, only SQLite is supported.
|
|
|
|
|
|
|
|
|
|
db_path: Path for the SQLite database
|
|
|
|
|
'''
|
|
|
|
|
_engine = sqlalchemy.create_engine('sqlite:///%s' % db_path)
|
|
|
|
|
|
|
|
|
|
_SQLBase.metadata.create_all(_engine)
|
|
|
|
|
_db_session_maker.configure(bind=_engine)
|
|
|
|
|
_configured = True
|
|
|
|
|
|
|
|
|
|
# TODO: Passing the session object is a little clunky. Maybe there's a way to infer it somehow?
|
|
|
|
|
def search(session, table_object, **constraints):
|
|
|
|
|
'''
|
|
|
|
|
Search the database for entries matching the given constraints.
|
|
|
|
|
|
|
|
|
|
session: SQLAlchemy session, presumably from get_db_session
|
|
|
|
|
table_object: SQLAlchemy ORM table object, defined in the file above
|
|
|
|
|
constraints: key-value pairs to match against specific fields in the database
|
|
|
|
|
Note: Currently, only the query.ilike method is supported. This is intended to
|
|
|
|
|
eventually support the entire range of available filters.
|
|
|
|
|
'''
|
|
|
|
|
query = session.query(table_object)
|
|
|
|
|
|
|
|
|
|
for key, value in constraints.items():
|
|
|
|
|
query = query.filter(getattr(table_object, key).ilike('%%%s%%' % value))
|
|
|
|
|
|
|
|
|
|
item_list = []
|
|
|
|
|
for item in query:
|
|
|
|
|
item_list.append(item)
|
|
|
|
|
|
|
|
|
|
return item_list
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
|
def get_db_session():
|
|
|
|
|
'''Get a SQLAlchemy database session with a proper context object. '''
|
|
|
|
|
# TODO: There's probably a more reliable way of knowing whether the database was configured.
|
|
|
|
|
if not _configured:
|
|
|
|
|
raise MetadataDBSessionException('Tried to get session without configuring a database.')
|
|
|
|
|
|
|
|
|
|
session = _db_session_maker()
|
|
|
|
|
try:
|
|
|
|
|
yield session
|
|
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
# TODO: Decide which exceptions to handle/eat here and which ones belong in UI.
|
|
|
|
|
# This one is okay to put off until you start really building UI.
|
|
|
|
|
session.rollback()
|
|
|
|
|
raise
|
|
|
|
|
else:
|
|
|
|
|
session.commit()
|
|
|
|
|
finally:
|
|
|
|
|
session.close()
|
|
|
|
|
class Release(_SQLBase):
|
|
|
|
|
'''SQLAlchemy ORM class for release metadata.'''
|
|
|
|
|
__tablename__ = 'releases'
|
|
|
|
|
id = sqlalchemy.Column(
|
|
|
|
|
sqlalchemy.Integer, sqlalchemy.Sequence('release_id_sequence'), primary_key=True
|
|
|
|
|
)
|
|
|
|
|
sha1sum = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False)
|
|
|
|
|
en_name = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False)
|
|
|
|
|
release_group_id = sqlalchemy.Column(
|
|
|
|
|
sqlalchemy.Integer, sqlalchemy.ForeignKey('release_groups.id')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
release_group = sqlalchemy.orm.relationship('ReleaseGroup', back_populates='releases')
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return ('Release: id: %s, en_name: %s' % (self.id, self.en_name )
|
|
|
|
|
|
|
|
|
|
class Image(_SQLBase):
|
|
|
|
|
'''SQLAlchemy ORM class for ROM image metadata.'''
|
|
|
|
|
__tablename__ = 'images'
|
|
|
|
|
id = sqlalchemy.Column(
|
|
|
|
|
sqlalchemy.Integer, sqlalchemy.Sequence('image_id_sequence'), primary_key=True
|
|
|
|
|
)
|
|
|
|
|
sha1sum = sqlalchemy.Column(sqlalchemy.String, unique=True, nullable=False)
|
|
|
|
|
format = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
|
|
|
|
region = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
|
|
|
|
version = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
|
|
|
|
disambiguation = sqlalchemy.Column(sqlalchemy.String)
|
|
|
|
|
release_group_id = sqlalchemy.Column(
|
|
|
|
|
sqlalchemy.Integer,sqlalchemy.ForeignKey('release_groups.id')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
release_group = sqlalchemy.orm.relationship('ReleaseGroup', back_populates='releases')
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
return (
|
|
|
|
|
'ROM Image: id: %s, sha1sum: %s, release-group: %s, region: %s, version: %s, '
|
|
|
|
|
'disambiguation: %s' % (
|
|
|
|
|
self.id, self.sha1sum, self.release_group.name, self.region, self.version,
|
|
|
|
|
self.disambiguation
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
class hashdb:
|
|
|
|
|
_engine = None
|
|
|
|
|
|
|
|
|
|
# TODO: db_path's default should be set here, not in frontend.
|
|
|
|
|
def __init__(self, db_path):
|
|
|
|
|
'''
|
|
|
|
|
Configure and initialize the database for the entire module.
|
|
|
|
|
Currently, only SQLite is supported.
|
|
|
|
|
|
|
|
|
|
db_path: Path for the SQLite database
|
|
|
|
|
'''
|
|
|
|
|
self._engine = sqlalchemy.create_engine('sqlite:///%s' % db_path)
|
|
|
|
|
|
|
|
|
|
_SQLBase.metadata.create_all(self._engine)
|
|
|
|
|
self._session_maker.configure(bind=self._engine)
|
|
|
|
|
|
|
|
|
|
def search(self, table_object, **constraints):
|
|
|
|
|
'''
|
|
|
|
|
Search the database for entries matching the given constraints.
|
|
|
|
|
|
|
|
|
|
table_object: SQLAlchemy ORM table object, defined in the file above
|
|
|
|
|
constraints: key-value pairs to match against specific fields in the database
|
|
|
|
|
Note: Currently, only the query.ilike method is supported. This is intended to
|
|
|
|
|
eventually support the entire range of available filters.
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
with self._get_db_session() as session:
|
|
|
|
|
# TODO: Consider making this return data recursively on items that reference other
|
|
|
|
|
# tables.
|
|
|
|
|
query = session.query(table_object)
|
|
|
|
|
|
|
|
|
|
for key, value in constraints.items():
|
|
|
|
|
query = query.filter(getattr(table_object, key).ilike('%%%s%%' % value))
|
|
|
|
|
|
|
|
|
|
item_list = []
|
|
|
|
|
for item in query:
|
|
|
|
|
item_list.append(item)
|
|
|
|
|
|
|
|
|
|
return item_list
|
|
|
|
|
|
|
|
|
|
def import_json(self, json_file):
|
|
|
|
|
'''
|
|
|
|
|
Import metadata from a json file.
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
# json files can be large, but not too large for ram
|
|
|
|
|
# do not exit on invalid metadata, log the error and skip the object
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
|
def _get_db_session():
|
|
|
|
|
'''Get a SQLAlchemy database session with a proper context object. '''
|
|
|
|
|
|
|
|
|
|
session = sqlalchemy.orm.sessionmaker()
|
|
|
|
|
try:
|
|
|
|
|
yield session
|
|
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
# TODO: Decide which exceptions to handle/eat here and which ones belong in UI.
|
|
|
|
|
# This one is okay to put off until you start really building UI.
|
|
|
|
|
session.rollback()
|
|
|
|
|
raise
|
|
|
|
|
else:
|
|
|
|
|
session.commit()
|
|
|
|
|
finally:
|
|
|
|
|
session.close()
|
|
|
|
|
|