diff --git a/.gitignore b/.gitignore index b6e4761..50947c3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ *.py[cod] *$py.class +*.password # C extensions *.so diff --git a/README.md b/README.md index b72d39d..f062598 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,16 @@ Clip#?, 00000_?, 00000, ... 00000_0_01.pes, 00000_0_01_?.pes, ?_slice?_clipXXXXX.pes, ... ## Properties: +* 新增对SUHD的支持 +* 新增对playitem自动排序 +* 新增自定义界面尺寸 * 不会重复导入已存在的轨道 * 自动寻找clip对应轨道 * 对共享clip的playItem,其时间应是相同的 * (Required) 使用前必须完成对所有素材的构建 -* (Required) 所有playitem都必须连接到clip上,所有clip都必须有video -* (Fixed!) 在删除轨道前需先清除playitem对轨道的引用 +* (Required) 所有playitem都必须链接到clip上,所有clip都必须有video * (X) 同一PlayList中重复clip的修改 * (X) Multiple angles * (X) PIP -* [TO DO] Relink playItem 2 clip +* (X) DV * [TO DO] PlayMark \ No newline at end of file diff --git a/Seamless-Branching-SBD.py b/Seamless-Branching-SBD.py index e3bdc2a..3aca023 100644 --- a/Seamless-Branching-SBD.py +++ b/Seamless-Branching-SBD.py @@ -5,15 +5,18 @@ from tkinter import messagebox import xml.etree.ElementTree as ET from assets import * +from medium import BluRay, UHDBluRay from timeutil import * from sorttracks import TrackSort from table import Table +from config import P class App: def __init__(self): self.ui = tk.Tk() - self.ui.title('Scenarist Ctrl Gui') + self.ui.title('Seamless Branching SBD/SUHD') + self.ui.protocol('WM_DELETE_WINDOW', self.close) self.showBasic() self.showButtons() @@ -26,9 +29,13 @@ def __init__(self): self.playItems:list[PlayItem] = [] self.clips:list[Clip] = [] - self.ui.bind('<>', self.updateInfo) + self.ui.bind('<>', self.updateInfo) self.ui.mainloop() + def close(self): + P.tojson() + self.ui.destroy() + def openSBDPRJ(self): sbdprjPath = filedialog.askopenfilename(title='Open Sbdprj Project', filetypes=[('sbdprj file', '*.sbdprj')]) if sbdprjPath: @@ -100,7 +107,8 @@ def showBasic(self): ttk.Label(gridT, text='PlayList').grid(row=0, column=0, padx=5, pady=(5, 2)) self.playListDropdown = ttk.Combobox(gridT, textvariable=self.playListVar, values=[], state='readonly') self.playListDropdown.grid(row=0, column=1, padx=5, pady=(5, 2)) - # ttk.Button(gridT, text='Import PlayItems from BDInfo', command=self.importBDInfo).grid(row=0, column=2, padx=5, pady=(5, 2)) + ttk.Button(gridT, text='Reorder PlayItems by BDInfo', width=25, command=self.reorderPlayItemByBDInfo) \ + .grid(row=0, column=2, padx=5, pady=(5, 2)) self.playListDropdown.bind('<>', self.selectPlayList) @@ -118,7 +126,11 @@ def showBasic(self): ttk.Label(gridB, text='Audio').grid(row=0, column=1, pady=(0, 2), sticky='n') ttk.Label(gridB, text='PG').grid(row=2, column=1, sticky='n') - self.tableAsset = ttk.Treeview(gridB1, columns=['type', 'name', 'index', 'pos'], displaycolumns=['type', 'name', 'index'], show='headings', height=20) + self.tableAsset = ttk.Treeview(gridB1, + columns=['type', 'name', 'index', 'pos'], + displaycolumns=['type', 'name', 'index'], + show='headings', + height=P.ui['Asset']['MaxRow']) # 20 self.tableAsset.grid(row=1, column=0, sticky='news') scrollY = ttk.Scrollbar(gridB1, orient='vertical', command=self.tableAsset.yview) scrollY.grid(row=1, column=1, sticky='ns') @@ -133,9 +145,15 @@ def showBasic(self): self.tableAsset.heading('index', text='index', command=lambda x=2: self.sortAsset(x)) self.tableAsset.column('index', width=50, stretch=False) - self.tableAudio = tk.Canvas(self.gridB2, width=560, height=170, scrollregion=(0, 0, 2000, 670)) + self.tableAudio = tk.Canvas(self.gridB2, + width=P.ui['Table']['Width'], # 560 + height=P.ui['Table']['Height'], # 170 + scrollregion=(0, 0, 2000, 670)) self.tableAudio.pack() - self.tablePG = tk.Canvas(self.gridB3, width=560, height=170, scrollregion=(0, 0, 2000, 670)) + self.tablePG = tk.Canvas(self.gridB3, + width=P.ui['Table']['Width'], # 560 + height=P.ui['Table']['Height'], # 170 + scrollregion=(0, 0, 2000, 670)) self.tablePG.pack() def showButtons(self): @@ -166,6 +184,8 @@ def showButtons(self): .grid(row=0, column=0, padx=4, pady=2, sticky='we') ttk.Button(gridB2, text='Set PG Language Code', command=self.setPGLanguageCode) \ .grid(row=1, column=0, padx=4, pady=2, sticky='we') + # ttk.Button(gridB2, text='Set Sequential PlayItems', command=self.setSeqPlayItem) \ + # .grid(row=2, column=0, padx=4, pady=2, sticky='we') ttk.Button(gridB2, text='Seamless Connection', command=self.setSeamless) \ .grid(row=3, column=0, padx=4, pady=2, sticky='we') @@ -247,10 +267,6 @@ def selectPlayList(self, event=None): tmp.append(si.languageCode + ',') self.pgData.append(tmp) # STN - self.audioOrder:dict[int, list] = {} - self.pgOrder:dict[int, list] = {} - self.audioOrderMeta:list[list[str, int, bool]] = [] - self.pgOrderMeta:list[list[str, int, bool]] = [] for i, pi in enumerate(self.playList.playItems): for ai in pi.playItemAudioInfs: for j, aj in enumerate(pi.clip.audios): @@ -342,6 +358,7 @@ def syncSlicedPGTimeInfo(self): if pts2Frame(pi.clip.pgs[sel['row']].duration, self.fps) > (pi.duration)/1875: print('[Warning] The PG clip is longer than clip#{} duration'.format(pi.clip.magicNum)) def syncIntactAudioTimeInfo(self): + now = 0 for sel in self.tableAudio.selected: pi = self.playList.playItems[sel['col']] # unit = pi.clip.audios[sel['row']].unitDuration @@ -354,8 +371,9 @@ def syncIntactAudioTimeInfo(self): pi.clip.clipAudioInfos[sel['row']].duration = dur else: raise ValueError('The audio clip in clip#{} is not long enough.'.format(pi.clip.magicNum)) - start = frame2PTS(pi.intime/1875, self.fps, unit=1) + start = frame2PTS(now/1875, self.fps, unit=1) pi.clip.clipAudioInfos[sel['row']].offset = start + now += pi.duration def hideAudio(self): data = [] @@ -374,7 +392,7 @@ def hidePG(self): data.append(sel['value'].split(',')[0] + ',') self.tablePG.setValue(data) def setAudioPID(self): - pid = input('Please input PID, it should look like these 4608, 0x1200: ') + pid = input('Please input PID, it should look like these 4352, 0x1100: ') if pid.startswith('0x'): pid = int(pid, base=16) else: @@ -390,7 +408,7 @@ def setAudioPID(self): data.append(sel['value']) self.tableAudio.setValue(data) def setPGPID(self): - pid = input('Please input PID, it should look like these 4608, 0x1200: ') + pid = input('Please input PID, it should look like these 4608, 4768, 0x1200, 0x12A0: ') if pid.startswith('0x'): pid = int(pid, base=16) else: @@ -517,31 +535,53 @@ def updateInfo(self, event=None): self.info['Duration'].set(clipPGInfo.duration) self.info['OffsetFromVideo'].set(clipPGInfo.offsetFromVideo) - def importBDInfo(self): - bdinfoPath = filedialog.askopenfilename(title='Open Sbdprj Project', filetypes=[('sbdprj file', '*.sbdprj')]) + def reorderPlayItemByBDInfo(self): + bdinfoPath = filedialog.askopenfilename(title='Open BDInfo TXT', filetypes=[('Text File', '*.txt')]) if bdinfoPath: - magicNums = [] + tmpPlayItems:list[PlayItem] = [] + tmpNodes: list[ET.Element] = [] + inSet = [] with open(bdinfoPath) as bdinfo: for line in bdinfo.readlines(): mn = line.strip()[:5] if len(mn) == 5 and mn.isnumeric(): - magicNums.append(mn) - if len(magicNums) != len(self.magicNums): - raise ValueError('Clip number is not correct.') - for i, mn in enumerate(magicNums): - for clip in self.clips: - if clip.magicNum == mn: - # how to deal with PID info in playItem - pi = self.playList.playItems[i] - pi.clip.delPlayItem(pi) - pi.setClip(clip) - pi.clip.setPlayItem(pi) - break + if mn in self.magicNums: + ind = self.magicNums.index(mn) + inSet.append(ind) + else: + print('[Warning] Cannot find Clip#{}'.format(mn)) + outSet = set(range(self.numOfClip)).difference(inSet) + for ind in inSet: + pi = self.playList.playItems[ind] + tmpPlayItems.append(pi) + node = self.playList.node.find('PlayItemIDList')[ind] + tmpNodes.append(node) + for ind in outSet: + pi = self.playList.playItems[ind] + tmpPlayItems.append(pi) + node = self.playList.node.find('PlayItemIDList')[ind] + tmpNodes.append(node) + while True: + for i in self.playList.node.find('PlayItemIDList'): + self.playList.node.find('PlayItemIDList').remove(i) + break else: - raise ValueError(f'Cannot find clip related to {mn}.m2ts') + break + self.playList.playItems = tmpPlayItems + self.playList.node.find('PlayItemIDList').extend(tmpNodes) self.selectPlayList() - + def setSeqPlayItem(self): + now = input('Please input PlayItem in time, default is 0: ') + try: + now = int(now) + except: + now = 0 + for pi in self.playList.playItems: + dur = pi.duration + pi.intime = now + pi.outtime = now + dur + now += dur def setSeamless(self): trigger = True for pi in self.playList.playItems: @@ -573,8 +613,12 @@ def setBDID(self): print('Success!') def unencrypt(self): - applicationVersion = self.discProjectInfo[0][0] - applicationVersion.text = '5.6.0.0000' + if P.medium.type_ == 'BluRay': + applicationVersion = self.discProjectInfo[0][0] + applicationVersion.text = '5.6.0.0000' + else: + applicationVersion = self.discProjectInfo[0][0] + applicationVersion.text = '8.0.0.0000' class MenuDisplay(tk.Menu): @@ -583,12 +627,14 @@ def __init__(self, master=None, parent:App=None): def version(): messagebox.showinfo('Version', 'Current Version: v0.0.1') def about(): - messagebox.showinfo('About', 'Version: v0.0.1\nAuthor: chaaaaang\nCopyright (c): 2022 chaaaaang') + messagebox.showinfo('About', 'Version: v0.1.0\nAuthor: chaaaaang\nCopyright (c): 2022 chaaaaang') self.parent = parent super().__init__(master, tearoff=False) master.config(menu=self) + self.mediumVar = tk.StringVar(value=P.medium.type_) + self.fileMenu = tk.Menu(master=self, tearoff=False) self.optionMenu = tk.Menu(master=self, tearoff=False) self.helpMenu = tk.Menu(master=self, tearoff=False) @@ -603,6 +649,9 @@ def about(): self.fileMenu.add_command(label='Close', command=self.close) self.optionMenu.add_command(label='Set BDID', command=self.setBDID) + self.optionMenu.add_separator() + self.optionMenu.add_radiobutton(label='BluRay', value='BluRay', variable=self.mediumVar, command=self.changeMedium) + self.optionMenu.add_radiobutton(label='UHD BluRay', value='UHD BluRay', variable=self.mediumVar, command=self.changeMedium) self.helpMenu.add_command(label='Version', command=version) self.helpMenu.add_command(label='About', command=about) @@ -623,11 +672,17 @@ def save(self): self.parent.tree.write(outputPath, encoding='utf-8', xml_declaration=True) def close(self): - self.parent.ui.destroy() + self.parent.close() def setBDID(self): self.parent.setBDID() + def changeMedium(self): + if self.mediumVar.get() == 'BluRay': + P.medium = BluRay() + else: + P.medium = UHDBluRay() + class TopUniversalDisplay(tk.Toplevel): def __init__(self, master=None, title='TopLevel', parent=None): @@ -668,6 +723,4 @@ def clickNo(self): self.clicked = 'No' self.master.event_generate('<>') - -if __name__ == '__main__': - App() \ No newline at end of file + \ No newline at end of file diff --git a/assets.py b/assets.py index e3dfca8..2d6e8a2 100644 --- a/assets.py +++ b/assets.py @@ -3,51 +3,8 @@ from typing import Union, Optional import os import re +from config import P -# for assets -def magicNum(x:str): - '''FilePath''' - tmp = os.path.split(x)[1] - mn = tmp[:5] - if mn.isnumeric(): - return mn -def index(x:str): - '''FilePath''' - tmp = os.path.split(x)[1] - mn = tmp[:5] - ind = x.split('.')[0][-2:] - if mn.isnumeric() and ind.isnumeric(): - return int(ind) -def magicNumPG(x:str): - '''FilePath''' - if 'slice' in x and 'clip' in x: - mn = x.split('.')[-2][-5:] - return mn - tmp = os.path.split(x)[1] - mn = tmp[:5] - if mn.isnumeric(): - return mn -def indexPG(x:str): - '''FilePath''' - if 'slice' in x and 'clip' in x: - return - try: - tmp = os.path.split(x)[1] - mn = tmp[:5] - ind = x.split('.')[0].split('_')[2] - if mn.isnumeric() and ind.isnumeric(): - return int(ind) - except: - pass -def magicNumClip(x:str): - '''Name''' - mn = x[:5] - if mn.isnumeric(): - return mn - match = re.search('Clip#(\d+)', x, flags=re.I) - if match: - mn = '%05d' % int(match.group(1)) - return mn class Video: @@ -58,8 +15,8 @@ def __init__(self, node:ET.Element): self.type = 'Video' self.id = node.attrib['ID'] self.name = node.find('Name').text - self.magicNum = magicNum(node.find('FilePath').text) - self.index = index(node.find('FilePath').text) + self.magicNum = P.medium.magicNum(node.find('FilePath').text) + self.index = P.medium.index(node.find('FilePath').text) self.languageCode = 'und' self.fps = self.FPS[node.find('FrameRate').text] @@ -70,8 +27,8 @@ def __init__(self, node:ET.Element): self.type = 'Audio' self.id = node.attrib['ID'] self.name = node.find('Name').text - self.magicNum = magicNum(node.find('FilePath').text) - self.index = index(node.find('FilePath').text) + self.magicNum = P.medium.magicNum(node.find('FilePath').text) + self.index = P.medium.index(node.find('FilePath').text) self.duration = int(node.find('Duration').text) tmp = node.find('UnitDuration') if tmp != None: @@ -94,8 +51,8 @@ def __init__(self, node:ET.Element): self.type = 'PG' self.id = node.attrib['ID'] self.name = node.find('Name').text - self.magicNum = magicNumPG(node.find('FilePath').text) - self.index = indexPG(node.find('FilePath').text) + self.magicNum = P.medium.magicNumPG(node.find('FilePath').text) + self.index = P.medium.indexPG(node.find('FilePath').text) self.start = int(node.find('StartTime').text) self.end = int(node.find('EndTime').text) self.duration = self.end - self.start @@ -135,7 +92,7 @@ class Clip: def __init__(self, node:ET.Element): self.node = node self.id = node.attrib['ID'] - self.magicNum = magicNumClip(node.find('Name').text) + self.magicNum = P.medium.magicNumClip(node.find('Name').text) self.playItems:list = [] # playList ID: playItem self.videos:list[Video] = [] self.audios:list[Audio] = [] @@ -236,7 +193,7 @@ def formatter(tag, content, tail=6): node.tail = '\n' + ' ' * 4 n1 = formatter('PlayListID', playItem.playListid, 6) n2 = formatter('PlayItemID', playItem.id, 6) - n3 = formatter('AngleID', '0', 5) # it should always be 0. At least seams so. + n3 = formatter('AngleID', '0', 5) # it should always be 0. At least seems so. node.extend([n1, n2, n3]) return node @@ -265,6 +222,7 @@ def getPGs(self, pgs:list[PG]): self.pgs.append(si) break + # unused def setPlayItem(self, playItem): for pl, pi in self.playItems: if pi is playItem: @@ -365,9 +323,6 @@ def __init__(self, node:ET.Element): self.node = node self.id = node.attrib['ID'] self.playListid = node.find('ParentObjectID') - self.intime = int(node.find('IN_time').text) - self.outtime = int(node.find('OUT_time').text) - self.duration = self.outtime - self.intime self.clip:Optional[Clip] = None n = node.find('STN_table_Block/Loop_PrimaryAudioStream') if len(n) == 0: @@ -388,6 +343,22 @@ def __init__(self, node:ET.Element): for i in node.find('STN_table_SS_Block/Loop_PG_textSTStream'): self.pgSSEntries.append(PlayItemStreamInf(i)) + @property + def intime(self): + return int(self.node.find('IN_time').text) + @intime.setter + def intime(self, value:int): + self.node.find('IN_time').text = str(value) + @property + def outtime(self): + return int(self.node.find('OUT_time').text) + @outtime.setter + def outtime(self, value:int): + self.node.find('OUT_time').text = str(value) + @property + def duration(self): + return self.outtime - self.intime + @property def connection(self): return self.node.find('connection_condition').text @@ -427,6 +398,7 @@ def getClip(self, clips:list[Clip]): self.clip = c c.playItems.append([self.playListid, self]) break + # unused def setClip(self, clip:Clip): self.node.find('clip_info_id_ref').text = clip.id self.clip = clip diff --git a/config.json b/config.json new file mode 100644 index 0000000..174f2d2 --- /dev/null +++ b/config.json @@ -0,0 +1,16 @@ +{ + "medium": "UHD BluRay", + "ui": { + "TrackSort": { + "MaxRow": 16 + }, + "Asset": { + "MaxRow": 20 + }, + "Table": { + "CellWidth": 63, + "Width": 560, + "Height": 170 + } + } +} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..46adc70 --- /dev/null +++ b/config.py @@ -0,0 +1,35 @@ +from medium import BluRay, UHDBluRay +import json +from typing import Union +import os + +class Config: + + def __init__(self): + if os.path.exists('./config.json'): + self.fromjson() + else: + self.medium = BluRay() + self.ui = { + 'TrackSort': {'MaxRow': 16}, + 'Asset': {'MaxRow': 20}, + 'Table': {'CellWidth': 63, + 'Width': 560, + 'Height': 170} + } + + def fromjson(self): + with open('./config.json') as f: + conf = json.load(f) + self.medium:Union[BluRay, UHDBluRay] = conf['medium'] == 'BluRay' and BluRay() or UHDBluRay() + self.ui = conf['ui'] + + def tojson(self): + conf = { + 'medium': self.medium.type_, + 'ui': self.ui + } + with open('./config.json', 'w') as f: + json.dump(conf, f, indent=4) + +P = Config() \ No newline at end of file diff --git a/login.py b/login.py new file mode 100644 index 0000000..8b00a38 --- /dev/null +++ b/login.py @@ -0,0 +1,40 @@ +import hashlib +import tkinter as tk +from tkinter import ttk +from tkinter import messagebox +gui = __import__('Seamless-Branching-SBD') + +class LoginPage: + + keyList = [ + '41dacd539d1b070cabb7b3f4f149a3e20df968e693599ae9b02aeede993862b2', + 'f54a54159e0edc184bc49032a881b0cf1f1b84ff2dd0d175ec38d652aa9c4d4c' + ] + + def __init__(self): + self.root = tk.Tk() + self.root.title('Login') + self.passwordVar = tk.StringVar(value='') + + gridT = ttk.Frame(self.root) + gridT.pack(fill='x', padx=20, pady=(30, 10)) + + ttk.Label(gridT, text='Password').pack(side='left', padx=(0, 10)) + ttk.Entry(gridT, textvariable=self.passwordVar).pack(side='right', padx=(5, 5)) + ttk.Button(self.root, text='Confirm', command=self.login).pack(pady=(10, 30)) + + self.root.mainloop() + + def login(self): + sha = hashlib.sha256() + sha.update(self.passwordVar.get().encode('utf_8')) + sha.update('SBSBD'.encode('utf_8')) + # print(sha.hexdigest()) + if sha.hexdigest() in self.keyList: + self.root.destroy() + gui.App() + else: + messagebox.showwarning('Seamless Branching Login', 'Wrong password!') + +if __name__ == '__main__': + LoginPage() \ No newline at end of file diff --git a/medium.py b/medium.py new file mode 100644 index 0000000..63009e7 --- /dev/null +++ b/medium.py @@ -0,0 +1,62 @@ +import os +import re + +class Medium: + + def __init__(self, type_='BluRay'): + self.type_ = type_ + + # for assets + def magicNum(self, x:str): + '''FilePath''' + tmp = os.path.split(x)[1] + mn = tmp[:5] + if mn.isnumeric(): + return mn + def index(self, x:str): + '''FilePath''' + tmp = os.path.split(x)[1] + mn = tmp[:5] + ind = x.split('.')[0][-2:] + if mn.isnumeric() and ind.isnumeric(): + return int(ind) + def magicNumPG(self, x:str): + '''FilePath''' + if 'slice' in x and 'clip' in x: + mn = x.split('.')[-2][-5:] + return mn + tmp = os.path.split(x)[1] + mn = tmp[:5] + if mn.isnumeric(): + return mn + def indexPG(self, x:str): + '''FilePath''' + if 'slice' in x and 'clip' in x: + return + try: + tmp = os.path.split(x)[1] + mn = tmp[:5] + ind = x.split('.')[0].split('_')[2] + if mn.isnumeric() and ind.isnumeric(): + return int(ind) + except: + pass + def magicNumClip(self, x:str): + '''Name''' + mn = x[:5] + if mn.isnumeric(): + return mn + match = re.search('Clip#(\d+)', x, flags=re.I) + if match: + mn = '%05d' % int(match.group(1)) + return mn + +class BluRay(Medium): + + def __init__(self): + super().__init__('BluRay') + +class UHDBluRay(Medium): + + def __init__(self): + super().__init__('UHD BluRay') diff --git a/sorttracks.py b/sorttracks.py index 6476c7b..630ce41 100644 --- a/sorttracks.py +++ b/sorttracks.py @@ -3,13 +3,14 @@ from tkinter import filedialog from assets import * from typing import Union +from config import P class TrackSort(ttk.Treeview): def __init__(self, master, magicNums:list[str], assets:Union[list[Audio],list[PG]]): self.assets = assets super().__init__(master, columns=['clip', 'asset'], - height=min(len(magicNums), 16), + height=min(len(magicNums), P.ui['TrackSort']['MaxRow']), # 16 selectmode='browse', show='headings') self.heading('clip', text='clip') diff --git a/table.py b/table.py index 8a8962e..162e3b4 100644 --- a/table.py +++ b/table.py @@ -2,6 +2,7 @@ from tkinter import ttk from tkinter import filedialog from tkinter import messagebox +from config import P class Table: @@ -22,7 +23,9 @@ def wheelX(event): a= int(-(event.delta)/60) self.canvas.xview_scroll(a, 'units') - maxwidth = 63 * (1 + self.colCount) + width = P.ui['Table']['Width'] + height = P.ui['Table']['Height'] + maxwidth = (1 + self.colCount) * P.ui['Table']['CellWidth'] self.canvas = tk.Canvas(master, width=width, height=height, scrollregion=(0, 0, maxwidth, 670)) self.middle = ttk.Frame(self.canvas) @@ -94,7 +97,7 @@ def select(self, event=None, colInd=None): row = event.widget.index(iid) value = event.widget.item(iid, 'values')[0] self.selectedNew = {'row': row, 'col': colInd, 'iid': iid, 'value': value} - self.rowHeadTab.event_generate('<>') + self.rowHeadTab.event_generate('<>') def selectAllClear(self): for coltab in self.tabs: tmp = coltab.selection()