Opt pattern matching
This commit is contained in:
@@ -44,9 +44,10 @@ class FileProperties():
|
||||
self.__sourceFilenameExtension = ''
|
||||
|
||||
self.__pc = PatternController(context)
|
||||
self.__usePattern = bool(self.context.get('use_pattern', True))
|
||||
|
||||
# Checking if database contains matching pattern
|
||||
matchResult = self.__pc.matchFilename(self.__sourceFilename)
|
||||
matchResult = self.__pc.matchFilename(self.__sourceFilename) if self.__usePattern else {}
|
||||
|
||||
self.__logger.debug(f"FileProperties.__init__(): Match result: {matchResult}")
|
||||
|
||||
|
||||
@@ -602,20 +602,21 @@ class MediaDetailsScreen(Screen):
|
||||
patternObj = self.getPatternObjFromInput()
|
||||
|
||||
if patternObj:
|
||||
patternId = self.__pc.addPattern(patternObj)
|
||||
mediaTags = {}
|
||||
for tagKey, tagValue in self.__sourceMediaDescriptor.getTags().items():
|
||||
|
||||
# Filter tags that make no sense to preserve
|
||||
if tagKey not in self.__ignoreGlobalKeys and not tagKey in self.__removeGlobalKeys:
|
||||
mediaTags[tagKey] = tagValue
|
||||
|
||||
patternId = self.__pc.savePatternSchema(
|
||||
patternObj,
|
||||
trackDescriptors=self.__sourceMediaDescriptor.getTrackDescriptors(),
|
||||
mediaTags=mediaTags,
|
||||
)
|
||||
if patternId:
|
||||
self.highlightPattern(False)
|
||||
|
||||
for tagKey, tagValue in self.__sourceMediaDescriptor.getTags().items():
|
||||
|
||||
# Filter tags that make no sense to preserve
|
||||
if tagKey not in self.__ignoreGlobalKeys and not tagKey in self.__removeGlobalKeys:
|
||||
self.__tac.updateMediaTag(patternId, tagKey, tagValue)
|
||||
|
||||
# for trackDescriptor in self.__sourceMediaDescriptor.getAllTrackDescriptors():
|
||||
for trackDescriptor in self.__sourceMediaDescriptor.getTrackDescriptors():
|
||||
self.__tc.addTrack(trackDescriptor, patternId = patternId)
|
||||
|
||||
|
||||
def action_new_pattern(self):
|
||||
"""Adding new patterns
|
||||
@@ -754,4 +755,3 @@ class MediaDetailsScreen(Screen):
|
||||
def handle_edit_pattern(self, screenResult):
|
||||
self.query_one("#pattern_input", Input).value = screenResult['pattern']
|
||||
self.updateDifferences()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import click
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from .show import Base, Show
|
||||
@@ -12,6 +12,9 @@ from ffx.show_descriptor import ShowDescriptor
|
||||
class Pattern(Base):
|
||||
|
||||
__tablename__ = 'patterns'
|
||||
__table_args__ = (
|
||||
UniqueConstraint('show_id', 'pattern', name='uq_patterns_show_id_pattern'),
|
||||
)
|
||||
|
||||
# v1.x
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
@@ -1,160 +1,388 @@
|
||||
import click, re
|
||||
import re
|
||||
|
||||
import click
|
||||
|
||||
from ffx.model.media_tag import MediaTag
|
||||
from ffx.model.pattern import Pattern
|
||||
from ffx.model.track import Track
|
||||
from ffx.model.track_tag import TrackTag
|
||||
from ffx.track_descriptor import TrackDescriptor
|
||||
from ffx.track_disposition import TrackDisposition
|
||||
|
||||
|
||||
class PatternController():
|
||||
|
||||
class DuplicatePatternMatchError(click.ClickException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidPatternSchemaError(click.ClickException):
|
||||
pass
|
||||
|
||||
|
||||
class PatternController:
|
||||
_compiled_regex_cache: dict[str, re.Pattern] = {}
|
||||
|
||||
def __init__(self, context):
|
||||
|
||||
|
||||
self.context = context
|
||||
self.Session = self.context['database']['session'] # convenience
|
||||
self.Session = self.context["database"]["session"]
|
||||
|
||||
self.__configurationData = self.context["config"].getData()
|
||||
|
||||
def addPattern(self, patternObj):
|
||||
"""Adds pattern to database from obj
|
||||
|
||||
Returns database id or 0 if pattern already exists"""
|
||||
metadataConfiguration = (
|
||||
self.__configurationData["metadata"]
|
||||
if "metadata" in self.__configurationData.keys()
|
||||
else {}
|
||||
)
|
||||
|
||||
self.__removeTrackKeys = (
|
||||
metadataConfiguration["streams"]["remove"]
|
||||
if "streams" in metadataConfiguration.keys()
|
||||
and "remove" in metadataConfiguration["streams"].keys()
|
||||
else []
|
||||
)
|
||||
self.__ignoreTrackKeys = (
|
||||
metadataConfiguration["streams"]["ignore"]
|
||||
if "streams" in metadataConfiguration.keys()
|
||||
and "ignore" in metadataConfiguration["streams"].keys()
|
||||
else []
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _clear_regex_cache(cls):
|
||||
cls._compiled_regex_cache.clear()
|
||||
|
||||
@classmethod
|
||||
def _compile_pattern_expression(cls, pattern_id: int, expression: str) -> re.Pattern:
|
||||
expression_text = str(expression)
|
||||
compiled = cls._compiled_regex_cache.get(expression_text)
|
||||
if compiled is None:
|
||||
try:
|
||||
compiled = re.compile(expression_text)
|
||||
except re.error as ex:
|
||||
raise click.ClickException(
|
||||
f"Pattern #{pattern_id} contains an invalid regex {expression_text!r}: {ex}"
|
||||
)
|
||||
cls._compiled_regex_cache[expression_text] = compiled
|
||||
return compiled
|
||||
|
||||
def _coerce_pattern_fields(self, patternObj):
|
||||
return {
|
||||
"show_id": int(patternObj["show_id"]),
|
||||
"pattern": str(patternObj["pattern"]),
|
||||
"quality": int(patternObj.get("quality", 0) or 0),
|
||||
"notes": str(patternObj.get("notes", "")),
|
||||
}
|
||||
|
||||
def _coerce_media_tags(self, mediaTags):
|
||||
return {
|
||||
str(tagKey): str(tagValue)
|
||||
for tagKey, tagValue in (mediaTags or {}).items()
|
||||
}
|
||||
|
||||
def _normalize_track_descriptors(self, trackDescriptors):
|
||||
if trackDescriptors is None:
|
||||
raise InvalidPatternSchemaError(
|
||||
"Patterns must define at least one track before they can be stored."
|
||||
)
|
||||
|
||||
normalized_descriptors = []
|
||||
for trackDescriptor in trackDescriptors:
|
||||
if type(trackDescriptor) is not TrackDescriptor:
|
||||
raise TypeError(
|
||||
"PatternController: All track descriptors are required to be of type TrackDescriptor"
|
||||
)
|
||||
normalized_descriptors.append(trackDescriptor)
|
||||
|
||||
if not normalized_descriptors:
|
||||
raise InvalidPatternSchemaError(
|
||||
"Patterns must define at least one track before they can be stored."
|
||||
)
|
||||
|
||||
normalized_descriptors = sorted(
|
||||
normalized_descriptors, key=lambda descriptor: descriptor.getIndex()
|
||||
)
|
||||
|
||||
index_set = {descriptor.getIndex() for descriptor in normalized_descriptors}
|
||||
expected_indexes = set(range(len(normalized_descriptors)))
|
||||
if index_set != expected_indexes:
|
||||
raise click.ClickException(
|
||||
"Pattern tracks must use a contiguous zero-based index order."
|
||||
)
|
||||
|
||||
return normalized_descriptors
|
||||
|
||||
def _ensure_unique_pattern_definition(
|
||||
self,
|
||||
session,
|
||||
show_id: int,
|
||||
pattern_expression: str,
|
||||
exclude_pattern_id: int | None = None,
|
||||
):
|
||||
query = session.query(Pattern).filter(
|
||||
Pattern.show_id == show_id,
|
||||
Pattern.pattern == pattern_expression,
|
||||
)
|
||||
if exclude_pattern_id is not None:
|
||||
query = query.filter(Pattern.id != int(exclude_pattern_id))
|
||||
|
||||
existing_pattern = query.first()
|
||||
if existing_pattern is not None:
|
||||
raise click.ClickException(
|
||||
f"Pattern {pattern_expression!r} already exists for show #{show_id}."
|
||||
)
|
||||
|
||||
def _build_track_row(self, trackDescriptor: TrackDescriptor) -> Track:
|
||||
track = Track(
|
||||
track_type=int(trackDescriptor.getType().index()),
|
||||
codec_name=str(trackDescriptor.getCodec().identifier()),
|
||||
index=int(trackDescriptor.getIndex()),
|
||||
source_index=int(trackDescriptor.getSourceIndex()),
|
||||
disposition_flags=int(
|
||||
TrackDisposition.toFlags(trackDescriptor.getDispositionSet())
|
||||
),
|
||||
audio_layout=trackDescriptor.getAudioLayout().index(),
|
||||
)
|
||||
|
||||
for tagKey, tagValue in trackDescriptor.getTags().items():
|
||||
if tagKey in self.__ignoreTrackKeys or tagKey in self.__removeTrackKeys:
|
||||
continue
|
||||
track.track_tags.append(TrackTag(key=str(tagKey), value=str(tagValue)))
|
||||
|
||||
return track
|
||||
|
||||
def _replace_pattern_schema(
|
||||
self,
|
||||
session,
|
||||
pattern: Pattern,
|
||||
mediaTags: dict[str, str],
|
||||
trackDescriptors: list[TrackDescriptor],
|
||||
):
|
||||
for mediaTag in list(pattern.media_tags):
|
||||
session.delete(mediaTag)
|
||||
for track in list(pattern.tracks):
|
||||
session.delete(track)
|
||||
session.flush()
|
||||
|
||||
for tagKey, tagValue in mediaTags.items():
|
||||
pattern.media_tags.append(MediaTag(key=str(tagKey), value=str(tagValue)))
|
||||
|
||||
for trackDescriptor in trackDescriptors:
|
||||
pattern.tracks.append(self._build_track_row(trackDescriptor))
|
||||
|
||||
def _validate_persisted_pattern(self, pattern: Pattern):
|
||||
if not pattern.tracks:
|
||||
raise InvalidPatternSchemaError(
|
||||
f"Pattern #{pattern.getId()} ({pattern.getPattern()!r}) is invalid because it has no tracks."
|
||||
)
|
||||
|
||||
def savePatternSchema(
|
||||
self,
|
||||
patternObj,
|
||||
trackDescriptors,
|
||||
mediaTags=None,
|
||||
patternId: int | None = None,
|
||||
) -> int:
|
||||
fields = self._coerce_pattern_fields(patternObj)
|
||||
normalized_tracks = self._normalize_track_descriptors(trackDescriptors)
|
||||
normalized_tags = self._coerce_media_tags(mediaTags)
|
||||
session = None
|
||||
|
||||
try:
|
||||
session = self.Session()
|
||||
self._ensure_unique_pattern_definition(
|
||||
session,
|
||||
fields["show_id"],
|
||||
fields["pattern"],
|
||||
exclude_pattern_id=patternId,
|
||||
)
|
||||
|
||||
s = self.Session()
|
||||
pattern = s.query(Pattern).filter(
|
||||
Pattern.show_id == int(patternObj['show_id']),
|
||||
Pattern.pattern == str(patternObj['pattern']),
|
||||
).first()
|
||||
|
||||
if pattern is None:
|
||||
pattern = Pattern(show_id = int(patternObj['show_id']),
|
||||
pattern = str(patternObj['pattern']))
|
||||
s.add(pattern)
|
||||
s.commit()
|
||||
return pattern.getId()
|
||||
if patternId is None:
|
||||
pattern = Pattern(
|
||||
show_id=fields["show_id"],
|
||||
pattern=fields["pattern"],
|
||||
quality=fields["quality"],
|
||||
notes=fields["notes"],
|
||||
)
|
||||
session.add(pattern)
|
||||
session.flush()
|
||||
else:
|
||||
return 0
|
||||
pattern = session.query(Pattern).filter(Pattern.id == int(patternId)).first()
|
||||
if pattern is None:
|
||||
raise click.ClickException(
|
||||
f"PatternController.savePatternSchema(): Pattern #{patternId} not found"
|
||||
)
|
||||
pattern.show_id = fields["show_id"]
|
||||
pattern.pattern = fields["pattern"]
|
||||
pattern.quality = fields["quality"]
|
||||
pattern.notes = fields["notes"]
|
||||
|
||||
self._replace_pattern_schema(
|
||||
session,
|
||||
pattern,
|
||||
normalized_tags,
|
||||
normalized_tracks,
|
||||
)
|
||||
|
||||
session.commit()
|
||||
self._clear_regex_cache()
|
||||
return pattern.getId()
|
||||
|
||||
except click.ClickException:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise click.ClickException(f"PatternController.addPattern(): {repr(ex)}")
|
||||
raise click.ClickException(
|
||||
f"PatternController.savePatternSchema(): {repr(ex)}"
|
||||
)
|
||||
finally:
|
||||
s.close()
|
||||
if session is not None:
|
||||
session.close()
|
||||
|
||||
def addPattern(self, patternObj, trackDescriptors=None, mediaTags=None):
|
||||
return self.savePatternSchema(
|
||||
patternObj,
|
||||
trackDescriptors=trackDescriptors,
|
||||
mediaTags=mediaTags,
|
||||
)
|
||||
|
||||
def updatePattern(self, patternId, patternObj):
|
||||
|
||||
fields = self._coerce_pattern_fields(patternObj)
|
||||
session = None
|
||||
|
||||
try:
|
||||
s = self.Session()
|
||||
pattern = s.query(Pattern).filter(Pattern.id == int(patternId)).first()
|
||||
session = self.Session()
|
||||
pattern = session.query(Pattern).filter(Pattern.id == int(patternId)).first()
|
||||
|
||||
if pattern is not None:
|
||||
self._ensure_unique_pattern_definition(
|
||||
session,
|
||||
fields["show_id"],
|
||||
fields["pattern"],
|
||||
exclude_pattern_id=patternId,
|
||||
)
|
||||
self._validate_persisted_pattern(pattern)
|
||||
|
||||
pattern.show_id = int(patternObj['show_id'])
|
||||
pattern.pattern = str(patternObj['pattern'])
|
||||
pattern.quality = str(patternObj['quality'])
|
||||
pattern.notes = str(patternObj['notes'])
|
||||
pattern.show_id = fields["show_id"]
|
||||
pattern.pattern = fields["pattern"]
|
||||
pattern.quality = fields["quality"]
|
||||
pattern.notes = fields["notes"]
|
||||
|
||||
s.commit()
|
||||
session.commit()
|
||||
self._clear_regex_cache()
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
except click.ClickException:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise click.ClickException(f"PatternController.updatePattern(): {repr(ex)}")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
if session is not None:
|
||||
session.close()
|
||||
|
||||
def findPattern(self, patternObj):
|
||||
|
||||
session = None
|
||||
|
||||
try:
|
||||
s = self.Session()
|
||||
pattern = s.query(Pattern).filter(
|
||||
Pattern.show_id == int(patternObj['show_id']),
|
||||
Pattern.pattern == str(patternObj['pattern']),
|
||||
).first()
|
||||
session = self.Session()
|
||||
pattern = (
|
||||
session.query(Pattern)
|
||||
.filter(
|
||||
Pattern.show_id == int(patternObj["show_id"]),
|
||||
Pattern.pattern == str(patternObj["pattern"]),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if pattern is not None:
|
||||
return int(pattern.id)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
except Exception as ex:
|
||||
raise click.ClickException(f"PatternController.findPattern(): {repr(ex)}")
|
||||
finally:
|
||||
s.close()
|
||||
if session is not None:
|
||||
session.close()
|
||||
|
||||
|
||||
def getPattern(self, patternId : int):
|
||||
def getPattern(self, patternId: int):
|
||||
|
||||
if type(patternId) is not int:
|
||||
raise ValueError(f"PatternController.getPattern(): Argument patternId is required to be of type int")
|
||||
raise ValueError(
|
||||
"PatternController.getPattern(): Argument patternId is required to be of type int"
|
||||
)
|
||||
|
||||
session = None
|
||||
try:
|
||||
s = self.Session()
|
||||
return s.query(Pattern).filter(Pattern.id == int(patternId)).first()
|
||||
session = self.Session()
|
||||
return session.query(Pattern).filter(Pattern.id == int(patternId)).first()
|
||||
|
||||
except Exception as ex:
|
||||
raise click.ClickException(f"PatternController.getPattern(): {repr(ex)}")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
if session is not None:
|
||||
session.close()
|
||||
|
||||
def deletePattern(self, patternId):
|
||||
session = None
|
||||
try:
|
||||
s = self.Session()
|
||||
pattern = s.query(Pattern).filter(Pattern.id == int(patternId)).first()
|
||||
session = self.Session()
|
||||
pattern = session.query(Pattern).filter(Pattern.id == int(patternId)).first()
|
||||
|
||||
if pattern is not None:
|
||||
|
||||
#DAFUQ: https://stackoverflow.com/a/19245058
|
||||
# q.delete()
|
||||
s.delete(pattern)
|
||||
|
||||
s.commit()
|
||||
session.delete(pattern)
|
||||
session.commit()
|
||||
self._clear_regex_cache()
|
||||
return True
|
||||
return False
|
||||
|
||||
except Exception as ex:
|
||||
raise click.ClickException(f"PatternController.deletePattern(): {repr(ex)}")
|
||||
finally:
|
||||
s.close()
|
||||
if session is not None:
|
||||
session.close()
|
||||
|
||||
|
||||
def matchFilename(self, filename : str) -> dict:
|
||||
"""Returns dict {'match': <a regex match obj>, 'pattern': <ffx pattern obj>} or empty dict of no pattern was found"""
|
||||
def matchFilename(self, filename: str) -> dict:
|
||||
"""Return {'match': regex match, 'pattern': Pattern} or {} when unmatched."""
|
||||
session = None
|
||||
|
||||
try:
|
||||
s = self.Session()
|
||||
q = s.query(Pattern)
|
||||
session = self.Session()
|
||||
matches = []
|
||||
query = session.query(Pattern).order_by(Pattern.show_id, Pattern.id)
|
||||
|
||||
matchResult = {}
|
||||
|
||||
for pattern in q.all():
|
||||
patternMatch = re.search(str(pattern.pattern), str(filename))
|
||||
if patternMatch is not None:
|
||||
matchResult['match'] = patternMatch
|
||||
matchResult['pattern'] = pattern
|
||||
for pattern in query.all():
|
||||
compiled = self._compile_pattern_expression(
|
||||
pattern.getId(),
|
||||
pattern.getPattern(),
|
||||
)
|
||||
patternMatch = compiled.search(str(filename))
|
||||
if patternMatch is None:
|
||||
continue
|
||||
|
||||
return matchResult
|
||||
|
||||
self._validate_persisted_pattern(pattern)
|
||||
matches.append({"match": patternMatch, "pattern": pattern})
|
||||
|
||||
if not matches:
|
||||
return {}
|
||||
|
||||
if len(matches) > 1:
|
||||
duplicateDescriptions = ", ".join(
|
||||
[
|
||||
f"show #{match['pattern'].getShowId()} pattern #{match['pattern'].getId()} {match['pattern'].getPattern()!r}"
|
||||
for match in matches
|
||||
]
|
||||
)
|
||||
raise DuplicatePatternMatchError(
|
||||
f"Filename {filename!r} matched more than one pattern: {duplicateDescriptions}"
|
||||
)
|
||||
|
||||
return matches[0]
|
||||
|
||||
except click.ClickException:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise click.ClickException(f"PatternController.matchFilename(): {repr(ex)}")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
# def getMediaDescriptor(self, context, patternId):
|
||||
#
|
||||
# try:
|
||||
# s = self.Session()
|
||||
# q = s.query(Pattern).filter(Pattern.id == int(patternId))
|
||||
#
|
||||
# if q.count():
|
||||
# return q.first().getMediaDescriptor(context)
|
||||
# else:
|
||||
# return None
|
||||
#
|
||||
# except Exception as ex:
|
||||
# raise click.ClickException(f"PatternController.getMediaDescriptor(): {repr(ex)}")
|
||||
# finally:
|
||||
# s.close()
|
||||
if session is not None:
|
||||
session.close()
|
||||
|
||||
@@ -6,7 +6,6 @@ from textual.widgets import Header, Footer, Static, Button, Input, DataTable, Te
|
||||
from textual.containers import Grid
|
||||
|
||||
from ffx.model.pattern import Pattern
|
||||
from ffx.model.track import Track
|
||||
|
||||
from .pattern_controller import PatternController
|
||||
from .show_controller import ShowController
|
||||
@@ -132,6 +131,8 @@ class PatternDetailsScreen(Screen):
|
||||
|
||||
self.__pattern : Pattern = self.__pc.getPattern(patternId) if patternId is not None else None
|
||||
self.__showDescriptor = self.__sc.getShowDescriptor(showId) if showId is not None else None
|
||||
self.__draftTracks : List[TrackDescriptor] = []
|
||||
self.__draftTags : dict[str, str] = {}
|
||||
|
||||
|
||||
#TODO: per controller
|
||||
@@ -158,42 +159,60 @@ class PatternDetailsScreen(Screen):
|
||||
|
||||
self.tracksTable.clear()
|
||||
|
||||
tracks = self.getCurrentTrackDescriptors()
|
||||
|
||||
typeCounter = {}
|
||||
|
||||
td: TrackDescriptor
|
||||
for td in tracks:
|
||||
|
||||
if (trackType := td.getType()) != TrackType.ATTACHMENT:
|
||||
|
||||
if not trackType in typeCounter.keys():
|
||||
typeCounter[trackType] = 0
|
||||
|
||||
dispoSet = td.getDispositionSet()
|
||||
|
||||
trackLanguage = td.getLanguage()
|
||||
audioLayout = td.getAudioLayout()
|
||||
|
||||
row = (td.getIndex(),
|
||||
trackType.label(),
|
||||
typeCounter[trackType],
|
||||
td.getCodec().label(),
|
||||
audioLayout.label() if trackType == TrackType.AUDIO
|
||||
and audioLayout != AudioLayout.LAYOUT_UNDEFINED else ' ',
|
||||
trackLanguage.label() if trackLanguage != IsoLanguage.UNDEFINED else ' ',
|
||||
td.getTitle(),
|
||||
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
|
||||
'Yes' if TrackDisposition.FORCED in dispoSet else 'No',
|
||||
td.getSourceIndex())
|
||||
|
||||
self.tracksTable.add_row(*map(str, row))
|
||||
|
||||
typeCounter[trackType] += 1
|
||||
|
||||
|
||||
def getCurrentTrackDescriptors(self) -> List[TrackDescriptor]:
|
||||
if self.__pattern is not None:
|
||||
return self.__tc.findSiblingDescriptors(self.__pattern.getId())
|
||||
return list(self.__draftTracks)
|
||||
|
||||
tracks = self.__tc.findTracks(self.__pattern.getId())
|
||||
|
||||
typeCounter = {}
|
||||
def normalizeDraftTracks(self):
|
||||
|
||||
tr: Track
|
||||
for tr in tracks:
|
||||
typeCounter = {}
|
||||
|
||||
td : TrackDescriptor = tr.getDescriptor(self.context)
|
||||
for index, trackDescriptor in enumerate(self.__draftTracks):
|
||||
trackDescriptor.setIndex(index)
|
||||
|
||||
if (trackType := td.getType()) != TrackType.ATTACHMENT:
|
||||
trackType = trackDescriptor.getType()
|
||||
subIndex = typeCounter.get(trackType, 0)
|
||||
trackDescriptor.setSubIndex(subIndex)
|
||||
typeCounter[trackType] = subIndex + 1
|
||||
|
||||
if not trackType in typeCounter.keys():
|
||||
typeCounter[trackType] = 0
|
||||
|
||||
dispoSet = td.getDispositionSet()
|
||||
|
||||
trackLanguage = td.getLanguage()
|
||||
audioLayout = td.getAudioLayout()
|
||||
|
||||
row = (td.getIndex(),
|
||||
trackType.label(),
|
||||
typeCounter[trackType],
|
||||
td.getCodec().label(),
|
||||
audioLayout.label() if trackType == TrackType.AUDIO
|
||||
and audioLayout != AudioLayout.LAYOUT_UNDEFINED else ' ',
|
||||
trackLanguage.label() if trackLanguage != IsoLanguage.UNDEFINED else ' ',
|
||||
td.getTitle(),
|
||||
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
|
||||
'Yes' if TrackDisposition.FORCED in dispoSet else 'No',
|
||||
td.getSourceIndex())
|
||||
|
||||
self.tracksTable.add_row(*map(str, row))
|
||||
|
||||
typeCounter[trackType] += 1
|
||||
if trackDescriptor.getSourceIndex() < 0:
|
||||
trackDescriptor.setSourceIndex(index)
|
||||
|
||||
|
||||
def swapTracks(self, trackIndex1: int, trackIndex2: int):
|
||||
@@ -201,6 +220,20 @@ class PatternDetailsScreen(Screen):
|
||||
ti1 = int(trackIndex1)
|
||||
ti2 = int(trackIndex2)
|
||||
|
||||
if self.__pattern is None:
|
||||
numSiblings = len(self.__draftTracks)
|
||||
|
||||
if ti1 < 0 or ti1 >= numSiblings:
|
||||
raise ValueError(f"PatternDetailsScreen.swapTracks(): trackIndex1 ({ti1}) is out of range ({numSiblings})")
|
||||
|
||||
if ti2 < 0 or ti2 >= numSiblings:
|
||||
raise ValueError(f"PatternDetailsScreen.swapTracks(): trackIndex2 ({ti2}) is out of range ({numSiblings})")
|
||||
|
||||
self.__draftTracks[ti1], self.__draftTracks[ti2] = self.__draftTracks[ti2], self.__draftTracks[ti1]
|
||||
self.normalizeDraftTracks()
|
||||
self.updateTracks()
|
||||
return
|
||||
|
||||
siblingDescriptors: List[TrackDescriptor] = self.__tc.findSiblingDescriptors(self.__pattern.getId())
|
||||
|
||||
numSiblings = len(siblingDescriptors)
|
||||
@@ -236,21 +269,22 @@ class PatternDetailsScreen(Screen):
|
||||
|
||||
self.tagsTable.clear()
|
||||
|
||||
if self.__pattern is not None:
|
||||
tags = (
|
||||
self.__tac.findAllMediaTags(self.__pattern.getId())
|
||||
if self.__pattern is not None
|
||||
else self.__draftTags
|
||||
)
|
||||
|
||||
tags = self.__tac.findAllMediaTags(self.__pattern.getId())
|
||||
for tagKey, tagValue in tags.items():
|
||||
|
||||
for tagKey, tagValue in tags.items():
|
||||
textColor = None
|
||||
if tagKey in self.__ignoreGlobalKeys:
|
||||
textColor = 'blue'
|
||||
if tagKey in self.__removeGlobalKeys:
|
||||
textColor = 'red'
|
||||
|
||||
textColor = None
|
||||
if tagKey in self.__ignoreGlobalKeys:
|
||||
textColor = 'blue'
|
||||
if tagKey in self.__removeGlobalKeys:
|
||||
textColor = 'red'
|
||||
|
||||
# if tagKey not in self.__ignoreTrackKeys:
|
||||
row = (formatRichColor(tagKey, textColor), formatRichColor(tagValue, textColor))
|
||||
self.tagsTable.add_row(*map(str, row))
|
||||
row = (formatRichColor(tagKey, textColor), formatRichColor(tagValue, textColor))
|
||||
self.tagsTable.add_row(*map(str, row))
|
||||
|
||||
|
||||
def on_mount(self):
|
||||
@@ -340,16 +374,9 @@ class PatternDetailsScreen(Screen):
|
||||
|
||||
# 9
|
||||
yield Static("Media Tags")
|
||||
|
||||
|
||||
if self.__pattern is not None:
|
||||
yield Button("Add", id="button_add_tag")
|
||||
yield Button("Edit", id="button_edit_tag")
|
||||
yield Button("Delete", id="button_delete_tag")
|
||||
else:
|
||||
yield Static(" ")
|
||||
yield Static(" ")
|
||||
yield Static(" ")
|
||||
yield Button("Add", id="button_add_tag")
|
||||
yield Button("Edit", id="button_edit_tag")
|
||||
yield Button("Delete", id="button_delete_tag")
|
||||
|
||||
yield Static(" ")
|
||||
yield Static(" ")
|
||||
@@ -363,16 +390,9 @@ class PatternDetailsScreen(Screen):
|
||||
|
||||
# 12
|
||||
yield Static("Streams")
|
||||
|
||||
|
||||
if self.__pattern is not None:
|
||||
yield Button("Add", id="button_add_track")
|
||||
yield Button("Edit", id="button_edit_track")
|
||||
yield Button("Delete", id="button_delete_track")
|
||||
else:
|
||||
yield Static(" ")
|
||||
yield Static(" ")
|
||||
yield Static(" ")
|
||||
yield Button("Add", id="button_add_track")
|
||||
yield Button("Edit", id="button_edit_track")
|
||||
yield Button("Delete", id="button_delete_track")
|
||||
|
||||
yield Static(" ")
|
||||
yield Button("Up", id="button_track_up")
|
||||
@@ -413,13 +433,8 @@ class PatternDetailsScreen(Screen):
|
||||
|
||||
def getSelectedTrackDescriptor(self):
|
||||
|
||||
if not self.__pattern:
|
||||
return None
|
||||
|
||||
try:
|
||||
|
||||
# Fetch the currently selected row when 'Enter' is pressed
|
||||
#selected_row_index = self.table.cursor_row
|
||||
row_key, col_key = self.tracksTable.coordinate_to_cell_key(self.tracksTable.cursor_coordinate)
|
||||
|
||||
if row_key is not None:
|
||||
@@ -428,10 +443,12 @@ class PatternDetailsScreen(Screen):
|
||||
trackIndex = int(selected_track_data[0])
|
||||
trackSubIndex = int(selected_track_data[2])
|
||||
|
||||
return self.__tc.getTrack(self.__pattern.getId(), trackIndex).getDescriptor(self.context, subIndex=trackSubIndex)
|
||||
for trackDescriptor in self.getCurrentTrackDescriptors():
|
||||
if (trackDescriptor.getIndex() == trackIndex
|
||||
and trackDescriptor.getSubIndex() == trackSubIndex):
|
||||
return trackDescriptor
|
||||
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
except CellDoesNotExist:
|
||||
return None
|
||||
@@ -482,7 +499,11 @@ class PatternDetailsScreen(Screen):
|
||||
self.app.pop_screen()
|
||||
|
||||
else:
|
||||
patternId = self.__pc.addPattern(patternDescriptor)
|
||||
patternId = self.__pc.savePatternSchema(
|
||||
patternDescriptor,
|
||||
trackDescriptors=self.__draftTracks,
|
||||
mediaTags=self.__draftTags,
|
||||
)
|
||||
if patternId:
|
||||
self.dismiss(patternDescriptor)
|
||||
else:
|
||||
@@ -494,33 +515,52 @@ class PatternDetailsScreen(Screen):
|
||||
self.app.pop_screen()
|
||||
|
||||
|
||||
# Save pattern when just created before adding streams
|
||||
if self.__pattern is not None:
|
||||
numTracks = len(self.getCurrentTrackDescriptors())
|
||||
|
||||
numTracks = len(self.tracksTable.rows)
|
||||
if event.button.id == "button_add_track":
|
||||
self.app.push_screen(
|
||||
TrackDetailsScreen(
|
||||
patternId=self.__pattern.getId() if self.__pattern is not None else None,
|
||||
patternLabel=self.getPatternFromInput(),
|
||||
siblingTrackDescriptors=self.getCurrentTrackDescriptors(),
|
||||
index=numTracks,
|
||||
),
|
||||
self.handle_add_track,
|
||||
)
|
||||
|
||||
if event.button.id == "button_add_track":
|
||||
self.app.push_screen(TrackDetailsScreen(patternId = self.__pattern.getId(), index = numTracks), self.handle_add_track)
|
||||
|
||||
selectedTrack = self.getSelectedTrackDescriptor()
|
||||
if selectedTrack is not None:
|
||||
if event.button.id == "button_edit_track":
|
||||
self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedTrack), self.handle_edit_track)
|
||||
if event.button.id == "button_delete_track":
|
||||
self.app.push_screen(TrackDeleteScreen(trackDescriptor = selectedTrack), self.handle_delete_track)
|
||||
selectedTrack = self.getSelectedTrackDescriptor()
|
||||
if selectedTrack is not None:
|
||||
if event.button.id == "button_edit_track":
|
||||
self.app.push_screen(
|
||||
TrackDetailsScreen(
|
||||
trackDescriptor=selectedTrack,
|
||||
patternId=self.__pattern.getId() if self.__pattern is not None else None,
|
||||
patternLabel=self.getPatternFromInput(),
|
||||
siblingTrackDescriptors=self.getCurrentTrackDescriptors(),
|
||||
),
|
||||
self.handle_edit_track,
|
||||
)
|
||||
if event.button.id == "button_delete_track":
|
||||
self.app.push_screen(
|
||||
TrackDeleteScreen(trackDescriptor = selectedTrack),
|
||||
self.handle_delete_track,
|
||||
)
|
||||
|
||||
|
||||
if event.button.id == "button_add_tag":
|
||||
if self.__pattern is not None:
|
||||
self.app.push_screen(TagDetailsScreen(), self.handle_update_tag)
|
||||
self.app.push_screen(TagDetailsScreen(), self.handle_update_tag)
|
||||
|
||||
if event.button.id == "button_edit_tag":
|
||||
tagKey, tagValue = self.getSelectedTag()
|
||||
self.app.push_screen(TagDetailsScreen(key=tagKey, value=tagValue), self.handle_update_tag)
|
||||
selectedTag = self.getSelectedTag()
|
||||
if selectedTag is not None:
|
||||
tagKey, tagValue = selectedTag
|
||||
self.app.push_screen(TagDetailsScreen(key=tagKey, value=tagValue), self.handle_update_tag)
|
||||
|
||||
if event.button.id == "button_delete_tag":
|
||||
tagKey, tagValue = self.getSelectedTag()
|
||||
self.app.push_screen(TagDeleteScreen(key=tagKey, value=tagValue), self.handle_delete_tag)
|
||||
selectedTag = self.getSelectedTag()
|
||||
if selectedTag is not None:
|
||||
tagKey, tagValue = selectedTag
|
||||
self.app.push_screen(TagDeleteScreen(key=tagKey, value=tagValue), self.handle_delete_tag)
|
||||
|
||||
|
||||
if event.button.id == "pattern_button":
|
||||
@@ -537,83 +577,106 @@ class PatternDetailsScreen(Screen):
|
||||
if event.button.id == "button_track_up":
|
||||
|
||||
selectedTrackDescriptor = self.getSelectedTrackDescriptor()
|
||||
selectedTrackIndex = selectedTrackDescriptor.getIndex()
|
||||
if selectedTrackDescriptor is not None:
|
||||
selectedTrackIndex = selectedTrackDescriptor.getIndex()
|
||||
|
||||
if selectedTrackIndex > 0 and selectedTrackIndex < self.tracksTable.row_count:
|
||||
correspondingTrackIndex = selectedTrackIndex - 1
|
||||
self.swapTracks(selectedTrackIndex, correspondingTrackIndex)
|
||||
if selectedTrackIndex > 0 and selectedTrackIndex < self.tracksTable.row_count:
|
||||
correspondingTrackIndex = selectedTrackIndex - 1
|
||||
self.swapTracks(selectedTrackIndex, correspondingTrackIndex)
|
||||
|
||||
|
||||
if event.button.id == "button_track_down":
|
||||
|
||||
selectedTrackDescriptor = self.getSelectedTrackDescriptor()
|
||||
selectedTrackIndex = selectedTrackDescriptor.getIndex()
|
||||
if selectedTrackDescriptor is not None:
|
||||
selectedTrackIndex = selectedTrackDescriptor.getIndex()
|
||||
|
||||
if selectedTrackIndex >= 0 and selectedTrackIndex < (self.tracksTable.row_count - 1):
|
||||
correspondingTrackIndex = selectedTrackIndex + 1
|
||||
self.swapTracks(selectedTrackIndex, correspondingTrackIndex)
|
||||
if selectedTrackIndex >= 0 and selectedTrackIndex < (self.tracksTable.row_count - 1):
|
||||
correspondingTrackIndex = selectedTrackIndex + 1
|
||||
self.swapTracks(selectedTrackIndex, correspondingTrackIndex)
|
||||
|
||||
|
||||
def handle_add_track(self, trackDescriptor : TrackDescriptor):
|
||||
if trackDescriptor is None:
|
||||
return
|
||||
|
||||
dispoSet = trackDescriptor.getDispositionSet()
|
||||
trackType = trackDescriptor.getType()
|
||||
index = trackDescriptor.getIndex()
|
||||
subIndex = trackDescriptor.getSubIndex()
|
||||
codec = trackDescriptor.getCodec()
|
||||
language = trackDescriptor.getLanguage()
|
||||
title = trackDescriptor.getTitle()
|
||||
if self.__pattern is not None:
|
||||
self.__tc.addTrack(trackDescriptor, patternId=self.__pattern.getId())
|
||||
else:
|
||||
self.__draftTracks.append(trackDescriptor)
|
||||
self.normalizeDraftTracks()
|
||||
|
||||
row = (index,
|
||||
trackType.label(),
|
||||
subIndex,
|
||||
codec.label(),
|
||||
language.label(),
|
||||
title,
|
||||
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
|
||||
'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
|
||||
|
||||
self.tracksTable.add_row(*map(str, row))
|
||||
self.updateTracks()
|
||||
|
||||
|
||||
def handle_edit_track(self, trackDescriptor : TrackDescriptor):
|
||||
if trackDescriptor is None:
|
||||
return
|
||||
|
||||
try:
|
||||
if self.__pattern is not None:
|
||||
if not self.__tc.updateTrack(trackDescriptor.getId(), trackDescriptor):
|
||||
raise click.ClickException("PatternDetailsScreen.handle_edit_track(): track update failed")
|
||||
else:
|
||||
selectedTrack = self.getSelectedTrackDescriptor()
|
||||
for index, currentTrack in enumerate(self.__draftTracks):
|
||||
if (selectedTrack is not None
|
||||
and currentTrack.getIndex() == selectedTrack.getIndex()
|
||||
and currentTrack.getSubIndex() == selectedTrack.getSubIndex()):
|
||||
self.__draftTracks[index] = trackDescriptor
|
||||
break
|
||||
self.normalizeDraftTracks()
|
||||
|
||||
row_key, col_key = self.tracksTable.coordinate_to_cell_key(self.tracksTable.cursor_coordinate)
|
||||
|
||||
self.tracksTable.update_cell(row_key, self.column_key_track_audio_layout,
|
||||
trackDescriptor.getAudioLayout().label()
|
||||
if trackDescriptor.getType() == TrackType.AUDIO else ' ')
|
||||
|
||||
self.tracksTable.update_cell(row_key, self.column_key_track_language, trackDescriptor.getLanguage().label())
|
||||
self.tracksTable.update_cell(row_key, self.column_key_track_title, trackDescriptor.getTitle())
|
||||
self.tracksTable.update_cell(row_key, self.column_key_track_default,
|
||||
'Yes' if TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() else 'No')
|
||||
self.tracksTable.update_cell(row_key, self.column_key_track_forced,
|
||||
'Yes' if TrackDisposition.FORCED in trackDescriptor.getDispositionSet() else 'No')
|
||||
|
||||
except CellDoesNotExist:
|
||||
pass
|
||||
self.updateTracks()
|
||||
|
||||
|
||||
def handle_delete_track(self, trackDescriptor : TrackDescriptor):
|
||||
if trackDescriptor is None:
|
||||
return
|
||||
|
||||
if self.__pattern is not None:
|
||||
track = self.__tc.getTrack(trackDescriptor.getPatternId(), trackDescriptor.getIndex())
|
||||
|
||||
if track is None:
|
||||
raise click.ClickException(
|
||||
f"Track is none: patternId={trackDescriptor.getPatternId()} type={trackDescriptor.getType()} subIndex={trackDescriptor.getSubIndex()}"
|
||||
)
|
||||
|
||||
self.__tc.deleteTrack(track.getId())
|
||||
else:
|
||||
self.__draftTracks = [
|
||||
currentTrack
|
||||
for currentTrack in self.__draftTracks
|
||||
if not (
|
||||
currentTrack.getIndex() == trackDescriptor.getIndex()
|
||||
and currentTrack.getSubIndex() == trackDescriptor.getSubIndex()
|
||||
)
|
||||
]
|
||||
self.normalizeDraftTracks()
|
||||
|
||||
self.updateTracks()
|
||||
|
||||
|
||||
|
||||
def handle_update_tag(self, tag):
|
||||
if tag is None:
|
||||
return
|
||||
|
||||
if self.__pattern is None:
|
||||
raise click.ClickException(f"PatternDetailsScreen.handle_update_tag: pattern not set")
|
||||
self.__draftTags[str(tag[0])] = str(tag[1])
|
||||
else:
|
||||
if self.__tac.updateMediaTag(self.__pattern.getId(), tag[0], tag[1]) is None:
|
||||
raise click.ClickException("PatternDetailsScreen.handle_update_tag(): tag update failed")
|
||||
|
||||
if self.__tac.updateMediaTag(self.__pattern.getId(), tag[0], tag[1]) is not None:
|
||||
self.updateTags()
|
||||
self.updateTags()
|
||||
|
||||
def handle_delete_tag(self, tag):
|
||||
if tag is None:
|
||||
return
|
||||
|
||||
if self.__pattern is None:
|
||||
raise click.ClickException(f"PatternDetailsScreen.handle_delete_tag: pattern not set")
|
||||
self.__draftTags.pop(str(tag[0]), None)
|
||||
self.updateTags()
|
||||
return
|
||||
|
||||
if self.__tac.deleteMediaTagByKey(self.__pattern.getId(), tag[0]):
|
||||
self.updateTags()
|
||||
|
||||
@@ -244,9 +244,15 @@ class TrackController():
|
||||
patternId = int(track.pattern_id)
|
||||
|
||||
q_siblings = s.query(Track).filter(Track.pattern_id == patternId).order_by(Track.index)
|
||||
siblingTracks = q_siblings.all()
|
||||
|
||||
if len(siblingTracks) <= 1:
|
||||
raise click.ClickException(
|
||||
f"Cannot delete the last track from pattern #{patternId}. Patterns must define at least one track."
|
||||
)
|
||||
|
||||
index = 0
|
||||
for track in q_siblings.all():
|
||||
for track in siblingTracks:
|
||||
|
||||
if track.id == int(trackId):
|
||||
s.delete(track)
|
||||
|
||||
@@ -6,8 +6,6 @@ from textual.containers import Grid
|
||||
|
||||
from ffx.track_descriptor import TrackDescriptor
|
||||
|
||||
from .track_controller import TrackController
|
||||
|
||||
|
||||
# Screen[dict[int, str, int]]
|
||||
class TrackDeleteScreen(Screen):
|
||||
@@ -52,14 +50,9 @@ class TrackDeleteScreen(Screen):
|
||||
def __init__(self, trackDescriptor : TrackDescriptor):
|
||||
super().__init__()
|
||||
|
||||
self.context = self.app.getContext()
|
||||
self.Session = self.context['database']['session'] # convenience
|
||||
|
||||
if type(trackDescriptor) is not TrackDescriptor:
|
||||
raise click.ClickException('TrackDeleteScreen.init(): trackDescriptor is required to be of type TrackDescriptor')
|
||||
|
||||
self.__tc = TrackController(context = self.context)
|
||||
|
||||
self.__trackDescriptor = trackDescriptor
|
||||
|
||||
|
||||
@@ -116,21 +109,7 @@ class TrackDeleteScreen(Screen):
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
|
||||
if event.button.id == "delete_button":
|
||||
|
||||
track = self.__tc.getTrack(self.__trackDescriptor.getPatternId(), self.__trackDescriptor.getIndex())
|
||||
|
||||
if track is None:
|
||||
raise click.ClickException(f"Track is none: patternId={self.__trackDescriptor.getPatternId()} type={self.__trackDescriptor.getType()} subIndex={self.__trackDescriptor.getSubIndex()}")
|
||||
|
||||
if track is not None:
|
||||
|
||||
if self.__tc.deleteTrack(track.getId()):
|
||||
self.dismiss(self.__trackDescriptor)
|
||||
|
||||
else:
|
||||
#TODO: Meldung
|
||||
self.app.pop_screen()
|
||||
self.dismiss(self.__trackDescriptor)
|
||||
|
||||
if event.button.id == "cancel_button":
|
||||
self.app.pop_screen()
|
||||
|
||||
|
||||
@@ -3,31 +3,20 @@ import click
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Header, Footer, Static, Button, SelectionList, Select, DataTable, Input
|
||||
from textual.containers import Grid
|
||||
|
||||
from ffx.model.pattern import Pattern
|
||||
|
||||
from .track_controller import TrackController
|
||||
from .pattern_controller import PatternController
|
||||
from .tag_controller import TagController
|
||||
|
||||
from .track_type import TrackType
|
||||
from .track_codec import TrackCodec
|
||||
|
||||
from .iso_language import IsoLanguage
|
||||
from .track_disposition import TrackDisposition
|
||||
from .audio_layout import AudioLayout
|
||||
|
||||
from .track_descriptor import TrackDescriptor
|
||||
|
||||
from .tag_details_screen import TagDetailsScreen
|
||||
from .tag_delete_screen import TagDeleteScreen
|
||||
|
||||
from textual.widgets._data_table import CellDoesNotExist
|
||||
|
||||
from .audio_layout import AudioLayout
|
||||
from .iso_language import IsoLanguage
|
||||
from .tag_delete_screen import TagDeleteScreen
|
||||
from .tag_details_screen import TagDetailsScreen
|
||||
from .track_codec import TrackCodec
|
||||
from .track_descriptor import TrackDescriptor
|
||||
from .track_disposition import TrackDisposition
|
||||
from .track_type import TrackType
|
||||
|
||||
from ffx.helper import formatRichColor, removeRichColor
|
||||
|
||||
|
||||
# Screen[dict[int, str, int]]
|
||||
class TrackDetailsScreen(Screen):
|
||||
|
||||
CSS = """
|
||||
@@ -79,7 +68,7 @@ class TrackDetailsScreen(Screen):
|
||||
.three {
|
||||
column-span: 3;
|
||||
}
|
||||
|
||||
|
||||
.four {
|
||||
column-span: 4;
|
||||
}
|
||||
@@ -97,257 +86,288 @@ class TrackDetailsScreen(Screen):
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, trackDescriptor : TrackDescriptor = None, patternId = None, trackType : TrackType = None, index = None, subIndex = None):
|
||||
def __init__(
|
||||
self,
|
||||
trackDescriptor: TrackDescriptor = None,
|
||||
patternId=None,
|
||||
patternLabel: str = "",
|
||||
siblingTrackDescriptors=None,
|
||||
trackType: TrackType = None,
|
||||
index=None,
|
||||
subIndex=None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.context = self.app.getContext()
|
||||
self.Session = self.context['database']['session'] # convenience
|
||||
|
||||
self.__configurationData = self.context['config'].getData()
|
||||
self.__configurationData = self.context["config"].getData()
|
||||
|
||||
metadataConfiguration = self.__configurationData['metadata'] if 'metadata' in self.__configurationData.keys() else {}
|
||||
metadataConfiguration = (
|
||||
self.__configurationData["metadata"]
|
||||
if "metadata" in self.__configurationData.keys()
|
||||
else {}
|
||||
)
|
||||
|
||||
self.__signatureTags = metadataConfiguration['signature'] if 'signature' in metadataConfiguration.keys() else {}
|
||||
self.__removeGlobalKeys = metadataConfiguration['remove'] if 'remove' in metadataConfiguration.keys() else []
|
||||
self.__ignoreGlobalKeys = metadataConfiguration['ignore'] if 'ignore' in metadataConfiguration.keys() else []
|
||||
self.__removeTrackKeys = (metadataConfiguration['streams']['remove']
|
||||
if 'streams' in metadataConfiguration.keys()
|
||||
and 'remove' in metadataConfiguration['streams'].keys() else [])
|
||||
self.__ignoreTrackKeys = (metadataConfiguration['streams']['ignore']
|
||||
if 'streams' in metadataConfiguration.keys()
|
||||
and 'ignore' in metadataConfiguration['streams'].keys() else [])
|
||||
|
||||
|
||||
self.__tc = TrackController(context = self.context)
|
||||
self.__pc = PatternController(context = self.context)
|
||||
self.__tac = TagController(context = self.context)
|
||||
self.__removeTrackKeys = (
|
||||
metadataConfiguration["streams"]["remove"]
|
||||
if "streams" in metadataConfiguration.keys()
|
||||
and "remove" in metadataConfiguration["streams"].keys()
|
||||
else []
|
||||
)
|
||||
self.__ignoreTrackKeys = (
|
||||
metadataConfiguration["streams"]["ignore"]
|
||||
if "streams" in metadataConfiguration.keys()
|
||||
and "ignore" in metadataConfiguration["streams"].keys()
|
||||
else []
|
||||
)
|
||||
|
||||
self.__isNew = trackDescriptor is None
|
||||
self.__trackDescriptor = trackDescriptor
|
||||
self.__patternId = (
|
||||
int(patternId)
|
||||
if patternId is not None
|
||||
else (
|
||||
int(trackDescriptor.getPatternId())
|
||||
if trackDescriptor is not None and trackDescriptor.getPatternId() != -1
|
||||
else -1
|
||||
)
|
||||
)
|
||||
self.__patternLabel = str(patternLabel)
|
||||
self.__siblingTrackDescriptors = list(siblingTrackDescriptors or [])
|
||||
|
||||
if self.__isNew:
|
||||
self.__trackType = trackType
|
||||
self.__trackCodec = TrackCodec.UNKNOWN
|
||||
self.__audioLayout = AudioLayout.LAYOUT_UNDEFINED
|
||||
self.__index = index
|
||||
self.__subIndex = subIndex
|
||||
self.__trackDescriptor : TrackDescriptor = None
|
||||
self.__pattern : Pattern = self.__pc.getPattern(patternId) if patternId is not None else {}
|
||||
self.__draftTrackTags = {}
|
||||
else:
|
||||
self.__trackType = trackDescriptor.getType()
|
||||
self.__trackCodec = trackDescriptor.getCodec()
|
||||
self.__audioLayout = trackDescriptor.getAudioLayout()
|
||||
self.__index = trackDescriptor.getIndex()
|
||||
self.__subIndex = trackDescriptor.getSubIndex()
|
||||
self.__trackDescriptor : TrackDescriptor = trackDescriptor
|
||||
self.__pattern : Pattern = self.__pc.getPattern(self.__trackDescriptor.getPatternId())
|
||||
|
||||
self.__draftTrackTags = {
|
||||
key: value
|
||||
for key, value in trackDescriptor.getTags().items()
|
||||
if key not in ("language", "title")
|
||||
}
|
||||
|
||||
def _descriptor_refs_same_track(self, descriptor: TrackDescriptor) -> bool:
|
||||
if self.__trackDescriptor is None:
|
||||
return False
|
||||
if descriptor.getId() != -1 and self.__trackDescriptor.getId() != -1:
|
||||
return descriptor.getId() == self.__trackDescriptor.getId()
|
||||
return (
|
||||
descriptor.getPatternId() == self.__trackDescriptor.getPatternId()
|
||||
and descriptor.getIndex() == self.__trackDescriptor.getIndex()
|
||||
and descriptor.getSubIndex() == self.__trackDescriptor.getSubIndex()
|
||||
)
|
||||
|
||||
def updateTags(self):
|
||||
|
||||
self.trackTagsTable.clear()
|
||||
|
||||
trackId = self.__trackDescriptor.getId()
|
||||
|
||||
if trackId != -1:
|
||||
|
||||
trackTags = self.__tac.findAllTrackTags(trackId)
|
||||
|
||||
for k,v in trackTags.items():
|
||||
|
||||
if k != 'language' and k != 'title':
|
||||
|
||||
textColor = None
|
||||
if k in self.__ignoreTrackKeys:
|
||||
textColor = 'blue'
|
||||
if k in self.__removeTrackKeys:
|
||||
textColor = 'red'
|
||||
|
||||
row = (formatRichColor(k, textColor), formatRichColor(v, textColor))
|
||||
self.trackTagsTable.add_row(*map(str, row))
|
||||
for key, value in self.__draftTrackTags.items():
|
||||
textColor = None
|
||||
if key in self.__ignoreTrackKeys:
|
||||
textColor = "blue"
|
||||
if key in self.__removeTrackKeys:
|
||||
textColor = "red"
|
||||
|
||||
row = (formatRichColor(key, textColor), formatRichColor(value, textColor))
|
||||
self.trackTagsTable.add_row(*map(str, row))
|
||||
|
||||
def on_mount(self):
|
||||
|
||||
self.query_one("#index_label", Static).update(str(self.__index) if self.__index is not None else '-')
|
||||
self.query_one("#subindex_label", Static).update(str(self.__subIndex)if self.__subIndex is not None else '-')
|
||||
|
||||
if self.__pattern is not None:
|
||||
self.query_one("#pattern_label", Static).update(self.__pattern.getPattern())
|
||||
self.query_one("#index_label", Static).update(
|
||||
str(self.__index) if self.__index is not None else "-"
|
||||
)
|
||||
self.query_one("#subindex_label", Static).update(
|
||||
str(self.__subIndex) if self.__subIndex is not None else "-"
|
||||
)
|
||||
self.query_one("#pattern_label", Static).update(self.__patternLabel)
|
||||
|
||||
if self.__trackType is not None:
|
||||
self.query_one("#type_select", Select).value = self.__trackType.label()
|
||||
if self.__trackType == TrackType.AUDIO:
|
||||
self.query_one("#audio_layout_select", Select).value = self.__audioLayout.label()
|
||||
|
||||
for d in TrackDisposition:
|
||||
self.query_one("#audio_layout_select", Select).value = self.__audioLayout.label()
|
||||
|
||||
dispositionIsSet = (self.__trackDescriptor is not None
|
||||
and d in self.__trackDescriptor.getDispositionSet())
|
||||
for disposition in TrackDisposition:
|
||||
|
||||
dispositionOption = (d.label(), d.index(), dispositionIsSet)
|
||||
self.query_one("#dispositions_selection_list", SelectionList).add_option(dispositionOption)
|
||||
dispositionIsSet = (
|
||||
self.__trackDescriptor is not None
|
||||
and disposition in self.__trackDescriptor.getDispositionSet()
|
||||
)
|
||||
|
||||
dispositionOption = (
|
||||
disposition.label(),
|
||||
disposition.index(),
|
||||
dispositionIsSet,
|
||||
)
|
||||
self.query_one("#dispositions_selection_list", SelectionList).add_option(
|
||||
dispositionOption
|
||||
)
|
||||
|
||||
if self.__trackDescriptor is not None:
|
||||
|
||||
self.query_one("#language_select", Select).value = self.__trackDescriptor.getLanguage().label()
|
||||
self.query_one("#language_select", Select).value = (
|
||||
self.__trackDescriptor.getLanguage().label()
|
||||
)
|
||||
self.query_one("#title_input", Input).value = self.__trackDescriptor.getTitle()
|
||||
self.updateTags()
|
||||
|
||||
|
||||
def compose(self):
|
||||
|
||||
self.trackTagsTable = DataTable(classes="five")
|
||||
|
||||
# Define the columns with headers
|
||||
self.column_key_track_tag_key = self.trackTagsTable.add_column("Key", width=50)
|
||||
self.column_key_track_tag_value = self.trackTagsTable.add_column("Value", width=100)
|
||||
|
||||
self.trackTagsTable.cursor_type = 'row'
|
||||
self.trackTagsTable.cursor_type = "row"
|
||||
|
||||
|
||||
languages = [l.label() for l in IsoLanguage]
|
||||
languages = [language.label() for language in IsoLanguage]
|
||||
|
||||
yield Header()
|
||||
|
||||
with Grid():
|
||||
|
||||
# 1
|
||||
yield Static(f"New stream" if self.__isNew else f"Edit stream", id="toplabel", classes="five")
|
||||
yield Static(
|
||||
"New stream" if self.__isNew else "Edit stream",
|
||||
id="toplabel",
|
||||
classes="five",
|
||||
)
|
||||
|
||||
# 2
|
||||
yield Static("for pattern")
|
||||
yield Static("", id="pattern_label", classes="four", markup=False)
|
||||
|
||||
# 3
|
||||
yield Static(" ", classes="five")
|
||||
|
||||
# 4
|
||||
yield Static("Index / Subindex")
|
||||
yield Static("", id="index_label", classes="two")
|
||||
yield Static("", id="subindex_label", classes="two")
|
||||
|
||||
# 5
|
||||
yield Static(" ", classes="five")
|
||||
|
||||
# 6
|
||||
yield Static("Type")
|
||||
yield Select.from_values([t.label() for t in TrackType], classes="four", id="type_select")
|
||||
yield Select.from_values(
|
||||
[trackType.label() for trackType in TrackType],
|
||||
classes="four",
|
||||
id="type_select",
|
||||
)
|
||||
|
||||
# 7
|
||||
if self.__trackType == TrackType.AUDIO:
|
||||
yield Static("Audio Layout")
|
||||
yield Select.from_values([t.label() for t in AudioLayout], classes="four", id="audio_layout_select")
|
||||
else:
|
||||
yield Static(" ", classes="five")
|
||||
yield Static("Audio Layout")
|
||||
yield Select.from_values(
|
||||
[layout.label() for layout in AudioLayout],
|
||||
classes="four",
|
||||
id="audio_layout_select",
|
||||
)
|
||||
|
||||
# 8
|
||||
yield Static(" ", classes="five")
|
||||
|
||||
# 9
|
||||
yield Static(" ", classes="five")
|
||||
|
||||
# 10
|
||||
yield Static("Language")
|
||||
yield Select.from_values(languages, classes="four", id="language_select")
|
||||
# 11
|
||||
|
||||
yield Static(" ", classes="five")
|
||||
|
||||
# 12
|
||||
yield Static("Title")
|
||||
yield Input(id="title_input", classes="four")
|
||||
|
||||
# 13
|
||||
yield Static(" ", classes="five")
|
||||
|
||||
# 14
|
||||
yield Static(" ", classes="five")
|
||||
|
||||
# 15
|
||||
yield Static("Stream tags")
|
||||
yield Static(" ")
|
||||
yield Button("Add", id="button_add_stream_tag")
|
||||
yield Button("Edit", id="button_edit_stream_tag")
|
||||
yield Button("Delete", id="button_delete_stream_tag")
|
||||
# 16
|
||||
|
||||
yield self.trackTagsTable
|
||||
|
||||
# 17
|
||||
yield Static(" ", classes="five")
|
||||
|
||||
# 18
|
||||
yield Static("Stream dispositions", classes="five")
|
||||
|
||||
# 19
|
||||
yield SelectionList[int](
|
||||
classes="five",
|
||||
id = "dispositions_selection_list"
|
||||
id="dispositions_selection_list",
|
||||
)
|
||||
|
||||
# 20
|
||||
yield Static(" ", classes="five")
|
||||
# 21
|
||||
yield Static(" ", classes="five")
|
||||
|
||||
# 22
|
||||
yield Button("Save", id="save_button")
|
||||
yield Button("Cancel", id="cancel_button")
|
||||
|
||||
# 23
|
||||
yield Static(" ", classes="five")
|
||||
|
||||
# 24
|
||||
yield Static(" ", classes="five", id="messagestatic")
|
||||
|
||||
|
||||
yield Footer(id="footer")
|
||||
|
||||
|
||||
def getTrackDescriptorFromInput(self):
|
||||
|
||||
kwargs = {}
|
||||
|
||||
kwargs[TrackDescriptor.CONTEXT_KEY] = self.context
|
||||
|
||||
kwargs[TrackDescriptor.PATTERN_ID_KEY] = int(self.__pattern.getId())
|
||||
if self.__trackDescriptor is not None and self.__trackDescriptor.getId() != -1:
|
||||
kwargs[TrackDescriptor.ID_KEY] = self.__trackDescriptor.getId()
|
||||
|
||||
kwargs[TrackDescriptor.INDEX_KEY] = self.__index
|
||||
kwargs[TrackDescriptor.SUB_INDEX_KEY] = self.__subIndex #!
|
||||
if self.__patternId != -1:
|
||||
kwargs[TrackDescriptor.PATTERN_ID_KEY] = int(self.__patternId)
|
||||
|
||||
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = TrackType.fromLabel(self.query_one("#type_select", Select).value)
|
||||
kwargs[TrackDescriptor.INDEX_KEY] = int(self.__index)
|
||||
kwargs[TrackDescriptor.SOURCE_INDEX_KEY] = (
|
||||
int(self.__trackDescriptor.getSourceIndex())
|
||||
if self.__trackDescriptor is not None
|
||||
else int(self.__index)
|
||||
)
|
||||
if self.__subIndex is not None and int(self.__subIndex) >= 0:
|
||||
kwargs[TrackDescriptor.SUB_INDEX_KEY] = int(self.__subIndex)
|
||||
|
||||
selectedTrackType = TrackType.fromLabel(
|
||||
self.query_one("#type_select", Select).value
|
||||
)
|
||||
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = selectedTrackType
|
||||
kwargs[TrackDescriptor.CODEC_KEY] = self.__trackCodec
|
||||
|
||||
if self.__trackType == TrackType.AUDIO:
|
||||
kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.fromLabel(self.query_one("#audio_layout_select", Select).value)
|
||||
|
||||
if selectedTrackType == TrackType.AUDIO:
|
||||
kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.fromLabel(
|
||||
self.query_one("#audio_layout_select", Select).value
|
||||
)
|
||||
else:
|
||||
kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.LAYOUT_UNDEFINED
|
||||
|
||||
trackTags = {}
|
||||
trackTags = dict(self.__draftTrackTags)
|
||||
|
||||
language = self.query_one("#language_select", Select).value
|
||||
if language:
|
||||
trackTags['language'] = IsoLanguage.find(language).threeLetter()
|
||||
trackTags["language"] = IsoLanguage.find(language).threeLetter()
|
||||
|
||||
title = self.query_one("#title_input", Input).value
|
||||
if title:
|
||||
trackTags['title'] = title
|
||||
trackTags["title"] = title
|
||||
|
||||
tableTags = {row[0]:row[1] for r in self.trackTagsTable.rows if (row := self.trackTagsTable.get_row(r)) and row[0] != 'language' and row[0] != 'title'}
|
||||
kwargs[TrackDescriptor.TAGS_KEY] = trackTags
|
||||
|
||||
kwargs[TrackDescriptor.TAGS_KEY] = trackTags | tableTags
|
||||
|
||||
dispositionFlags = sum([2**f for f in self.query_one("#dispositions_selection_list", SelectionList).selected])
|
||||
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = TrackDisposition.toSet(dispositionFlags)
|
||||
dispositionFlags = sum(
|
||||
[2 ** flag for flag in self.query_one("#dispositions_selection_list", SelectionList).selected]
|
||||
)
|
||||
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = TrackDisposition.toSet(
|
||||
dispositionFlags
|
||||
)
|
||||
|
||||
return TrackDescriptor(**kwargs)
|
||||
|
||||
|
||||
|
||||
def getSelectedTag(self):
|
||||
|
||||
try:
|
||||
|
||||
# Fetch the currently selected row when 'Enter' is pressed
|
||||
#selected_row_index = self.table.cursor_row
|
||||
row_key, col_key = self.trackTagsTable.coordinate_to_cell_key(self.trackTagsTable.cursor_coordinate)
|
||||
row_key, _ = self.trackTagsTable.coordinate_to_cell_key(
|
||||
self.trackTagsTable.cursor_coordinate
|
||||
)
|
||||
|
||||
if row_key is not None:
|
||||
selected_tag_data = self.trackTagsTable.get_row(row_key)
|
||||
@@ -357,101 +377,92 @@ class TrackDetailsScreen(Screen):
|
||||
|
||||
return tagKey, tagValue
|
||||
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
except CellDoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
# Event handler for button press
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
|
||||
# Check if the button pressed is the one we are interested in
|
||||
if event.button.id == "save_button":
|
||||
|
||||
# Check for multiple default/forced disposition flags
|
||||
|
||||
if self.__trackType == TrackType.VIDEO:
|
||||
trackList = self.__tc.findVideoTracks(self.__pattern.getId())
|
||||
if self.__trackType == TrackType.AUDIO:
|
||||
trackList = self.__tc.findAudioTracks(self.__pattern.getId())
|
||||
elif self.__trackType == TrackType.SUBTITLE:
|
||||
trackList = self.__tc.findSubtitleTracks(self.__pattern.getId())
|
||||
else:
|
||||
trackList = []
|
||||
|
||||
siblingTrackList = [t for t in trackList if t.getType() == self.__trackType and t.getIndex() != self.__index]
|
||||
|
||||
numDefaultTracks = len([t for t in siblingTrackList if TrackDisposition.DEFAULT in t.getDispositionSet()])
|
||||
numForcedTracks = len([t for t in siblingTrackList if TrackDisposition.FORCED in t.getDispositionSet()])
|
||||
|
||||
self.__subIndex = len(trackList)
|
||||
trackDescriptor = self.getTrackDescriptorFromInput()
|
||||
|
||||
if ((TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() and numDefaultTracks)
|
||||
or (TrackDisposition.FORCED in trackDescriptor.getDispositionSet() and numForcedTracks)):
|
||||
siblingTrackList = [
|
||||
descriptor
|
||||
for descriptor in self.__siblingTrackDescriptors
|
||||
if not self._descriptor_refs_same_track(descriptor)
|
||||
]
|
||||
siblingTrackList = [
|
||||
descriptor
|
||||
for descriptor in siblingTrackList
|
||||
if descriptor.getType() == trackDescriptor.getType()
|
||||
]
|
||||
|
||||
self.query_one("#messagestatic", Static).update("Cannot add another stream with disposition flag 'debug' or 'forced' set")
|
||||
numDefaultTracks = len(
|
||||
[
|
||||
descriptor
|
||||
for descriptor in siblingTrackList
|
||||
if TrackDisposition.DEFAULT in descriptor.getDispositionSet()
|
||||
]
|
||||
)
|
||||
numForcedTracks = len(
|
||||
[
|
||||
descriptor
|
||||
for descriptor in siblingTrackList
|
||||
if TrackDisposition.FORCED in descriptor.getDispositionSet()
|
||||
]
|
||||
)
|
||||
|
||||
if self.__isNew:
|
||||
trackDescriptor.setSubIndex(len(siblingTrackList))
|
||||
elif self.__subIndex is not None and int(self.__subIndex) >= 0:
|
||||
trackDescriptor.setSubIndex(int(self.__subIndex))
|
||||
|
||||
if (
|
||||
TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet()
|
||||
and numDefaultTracks
|
||||
) or (
|
||||
TrackDisposition.FORCED in trackDescriptor.getDispositionSet()
|
||||
and numForcedTracks
|
||||
):
|
||||
|
||||
self.query_one("#messagestatic", Static).update(
|
||||
"Cannot add another stream with disposition flag 'default' or 'forced' set"
|
||||
)
|
||||
else:
|
||||
|
||||
self.query_one("#messagestatic", Static).update(" ")
|
||||
|
||||
if self.__isNew:
|
||||
|
||||
# Track per Screen hinzufügen
|
||||
self.__tc.addTrack(trackDescriptor)
|
||||
self.dismiss(trackDescriptor)
|
||||
|
||||
else:
|
||||
|
||||
track = self.__tc.getTrack(self.__pattern.getId(), self.__index)
|
||||
|
||||
# Track per details screen updaten
|
||||
if self.__tc.updateTrack(track.getId(), trackDescriptor):
|
||||
self.dismiss(trackDescriptor)
|
||||
|
||||
else:
|
||||
self.app.pop_screen()
|
||||
self.dismiss(trackDescriptor)
|
||||
|
||||
if event.button.id == "cancel_button":
|
||||
self.app.pop_screen()
|
||||
|
||||
|
||||
if event.button.id == "button_add_stream_tag":
|
||||
if not self.__isNew:
|
||||
self.app.push_screen(TagDetailsScreen(), self.handle_update_tag)
|
||||
self.app.push_screen(TagDetailsScreen(), self.handle_update_tag)
|
||||
|
||||
if event.button.id == "button_edit_stream_tag":
|
||||
tagKey, tagValue = self.getSelectedTag()
|
||||
self.app.push_screen(TagDetailsScreen(key=tagKey, value=tagValue), self.handle_update_tag)
|
||||
selectedTag = self.getSelectedTag()
|
||||
if selectedTag is not None:
|
||||
self.app.push_screen(
|
||||
TagDetailsScreen(key=selectedTag[0], value=selectedTag[1]),
|
||||
self.handle_update_tag,
|
||||
)
|
||||
|
||||
if event.button.id == "button_delete_stream_tag":
|
||||
tagKey, tagValue = self.getSelectedTag()
|
||||
self.app.push_screen(TagDeleteScreen(key=tagKey, value=tagValue), self.handle_delete_tag)
|
||||
|
||||
selectedTag = self.getSelectedTag()
|
||||
if selectedTag is not None:
|
||||
self.app.push_screen(
|
||||
TagDeleteScreen(key=selectedTag[0], value=selectedTag[1]),
|
||||
self.handle_delete_tag,
|
||||
)
|
||||
|
||||
def handle_update_tag(self, tag):
|
||||
|
||||
trackId = self.__trackDescriptor.getId()
|
||||
|
||||
if trackId == -1:
|
||||
raise click.ClickException(f"TrackDetailsScreen.handle_update_tag: trackId not set (-1) trackDescriptor={self.__trackDescriptor}")
|
||||
|
||||
if self.__tac.updateTrackTag(trackId, tag[0], tag[1]) is not None:
|
||||
self.updateTags()
|
||||
if tag is None:
|
||||
return
|
||||
self.__draftTrackTags[str(tag[0])] = str(tag[1])
|
||||
self.updateTags()
|
||||
|
||||
def handle_delete_tag(self, trackTag):
|
||||
|
||||
trackId = self.__trackDescriptor.getId()
|
||||
|
||||
if trackId == -1:
|
||||
raise click.ClickException(f"TrackDetailsScreen.handle_delete_tag: trackId not set (-1) trackDescriptor={self.__trackDescriptor}")
|
||||
|
||||
tag = self.__tac.findTrackTag(trackId, trackTag[0])
|
||||
|
||||
if tag is not None:
|
||||
if self.__tac.deleteTrackTag(tag.id):
|
||||
self.updateTags()
|
||||
if trackTag is None:
|
||||
return
|
||||
self.__draftTrackTags.pop(str(trackTag[0]), None)
|
||||
self.updateTags()
|
||||
|
||||
Reference in New Issue
Block a user