import { Track } from "./tracks_table.mjs" import { findLanguage, isLanguageTagAlias, flattenObj, updateObjectFromJoinedKey, LANGUAGES } from "./utils.mjs" class Mismatch { constructor(track, key, value) { /** @type {Track} */ this.track = track /** @type {string} */ this.key = key this.value = value } } const CorrectionType = { NOTHING: "nothing", NAME: "name", FIELD: "field" } const WORDS = { forced: { default: "Forced", fre: "Forcés", "fre-ca": "Forcés" }, full: { default: "Full", fre: "Complets", "fre-ca": "Complets" }, } function containsWord(parts, word) { return Object.values(WORDS[word]).some(w => parts.includes(w)) } function getWord(word, lang) { const words = WORDS[word] return words[lang] ?? words.default } class MismatchCorrection { /** * * @param {Mismatch} mismatch * @param {string} oldName * @param {string} newName */ constructor(mismatch, oldName, newName) { this.track = mismatch.track this.key = mismatch.key this.oldName = oldName this.newName = newName this.oldValue = this.track.fields[mismatch.key] this.newValue = mismatch.value } apply(correctionType) { switch (correctionType) { case CorrectionType.NOTHING: break case CorrectionType.NAME: this.track.editValue("name", this.newName) break case CorrectionType.FIELD: this.track.editValue(this.key, this.newValue) break default: throw new Error( `Invalid correction type, must be one of [${Object.values(CorrectionType)}], got ${correctionType}` ) } } } export default class IntegrityManager { IGNORE_KEYS = [ "type", "channels_details" ] /** * * @param {import('./editor.mjs').default} editor */ constructor(editor) { this.editor = editor /** @type {Mismatch[]} */ this.mismatches = [] /** @type {?MismatchCorrection} */ this.correction = null this.ignoreList = [] this.popup = document.getElementById("integrity-popup") this.popup.querySelectorAll(".correction .option").forEach(opt => { const radio = opt.querySelector("input[type='radio']") opt.addEventListener("click", e => { radio.click() }) radio.addEventListener("input", () => { if (radio.checked) { const prev = this.popup.querySelector(".option.selected input[type='radio']:not(:checked)") prev.parentElement.classList.remove("selected") opt.classList.add("selected") } else { opt.classList.remove("selected") } }) if (radio.checked) { opt.classList.add("selected") } }) this.popup.querySelector("#int-apply").addEventListener("click", () => { const fd = new FormData(this.popup.querySelector("form")) const correctionType = fd.get("int-corr-type") this.correct(correctionType) }) } nextError() { if (this.mismatches.length === 0) { this.hideMismatchPopup() return } const mismatch = this.mismatches.shift() console.log("Next mismatch:", mismatch) this.mismatches = this.mismatches.filter(m => m.track !== mismatch.track) this.showMismatchPopup(mismatch) } checkIntegrity() { this.ignoreList = [] this.mismatches = [] for (const table of Object.values(this.editor.tables)) { this.checkTableIntegrity(table) } if (this.mismatches.length === 0) { return true } this.nextError() return false } /** * * @param {import('./tracks_table.mjs').default} table */ checkTableIntegrity(table) { for (const track of table.tracks) { this.checkTrackIntegrity(track) } } /** * * @param {Track} track */ checkTrackIntegrity(track, prepend=false) { let fields = this.parseName(track.table.type, track.fields["name"]) fields = flattenObj(fields) Object.entries(fields).map(([key, value]) => { if (this.IGNORE_KEYS.includes(key)) { return } if (this.ignoreList.some(o => o.track === track && o.key === key)) { return } let equal = track.fields[key] === value if (key === "language") { equal = isLanguageTagAlias(value, track.fields[key]) } if (!equal) { this.addMismatchField(track, key, value, prepend) //console.error(`Mismatch for field ${key}:\n- name: ${value}\n- track: ${track.fields[key]}`) } else { track.fields[key] = value } }) } parseName(trackType, name) { /** @type {string} */ const lower = name.toLowerCase() const parts = lower.split(/\b/) const fields = {flags: {}} switch (trackType) { case "audio": const audioLang = findLanguage(lower) if (audioLang !== null) {fields.language = audioLang} const original = parts.includes("vo") if (original) {fields.flags.original = original} const ad = parts.includes("ad") if (ad) {fields.flags.visual_impaired = ad} const channels = lower.match(/\d+\.\d+/) if (channels) { fields.channels_details = channels[0] } break case "subtitle": const stLang = findLanguage(lower) if (stLang !== null) {fields.language = stLang} const isForced = containsWord(parts, "forced") const isFull = containsWord(parts, "full") if (isForced) { fields.flags.forced = true } else if (isFull) { fields.flags.forced = false } const sdh = parts.includes("sdh") if (sdh) {fields.flags.hearing_impaired = sdh} if (parts.includes("pgs")) { fields.type = "PGS" } else { fields.type = "SRT" } break } return fields } addMismatchField(track, key, value, prepend=false) { const mismatch = new Mismatch(track, key, value) if (prepend) { this.mismatches.splice(0, 0, mismatch) } else { this.mismatches.push(mismatch) } } /** * * @param {Mismatch} mismatch */ showMismatchPopup(mismatch) { const q = sel => this.popup.querySelector(sel) const track = mismatch.track const table = track.table const fieldProps = table.getFieldProps(mismatch.key) const ogName = track.fields["name"] const ogValue = track.fields[mismatch.key] let fields = this.parseName(table.type, ogName) let keys = Object.keys(flattenObj(fields)) Object.entries(flattenObj(track.fields)).forEach(([key, val]) => { if (!keys.includes(key) || key == mismatch.key) { updateObjectFromJoinedKey(fields, key, val) } }) const newName = this.reconstructName(table.type, fields) this.correction = new MismatchCorrection( mismatch, ogName, newName ) let ogField = track.makeInput(fieldProps, ogValue, false) let newField = track.makeInput(fieldProps, mismatch.value, false) ogField = this.lockInput(ogField) newField = this.lockInput(newField) ogField.id = "int-og-field" newField.id = "int-corr-new-field" q("#int-track-type").innerText = table.type q("#int-track-idx").innerText = track.idx q("#int-field-name").innerText = fieldProps.name q("#int-og-name").value = ogName q("#int-og-field-name").innerText = fieldProps.name q("#int-og-field").replaceWith(ogField) q("#int-corr-new-name").value = newName q("#int-corr-new-field-name").innerText = fieldProps.name q("#int-corr-new-field").replaceWith(newField) this.popup.classList.add("show") } reconstructName(tableType, fields) { let name = LANGUAGES[fields.language].display switch (tableType) { case "audio": if (fields.flags.original) { name += " VO" }/* else if (fields.language === "fre") { name += " VFF" } else if (fields.language === "fre-ca") { name += " VFQ" }*/ if (fields.flags.visual_impaired) { name += " AD" } if (fields.channels_details) { name += " / " + fields.channels_details } break case "subtitle": if (fields.flags.forced) { name += " " + getWord("forced", fields.language) } else { name += " " + getWord("full", fields.language) } if (fields.flags.hearing_impaired) { name += " SDH" } name += " : " + fields.type break } return name } hideMismatchPopup() { this.popup.classList.remove("show") } correct(correctionType) { if (correctionType === CorrectionType.NOTHING) { this.ignoreList.push({ track: this.correction.track, key: this.correction.key }) } this.correction.apply(correctionType) this.checkTrackIntegrity(this.correction.track, true) this.nextError() } lockInput(input) { input.readOnly = true if (input.type === "checkbox") { input.disabled = true } else if (input.tagName === "SELECT") { let text = document.createElement("div") text.classList.add("select") text.innerText = input.value text.dataset.key = input.dataset.key input = text } return input } improveAllNames() { for (const table of Object.values(this.editor.tables)) { for (const track of table.tracks) { this.improveName(track) } } } /** * * @param {Track} track */ improveName(track) { let nameFields = this.parseName(track.table.type, track.fields["name"]) const keys = Object.keys(flattenObj(nameFields)) Object.entries(flattenObj(track.fields)).forEach(([key, val]) => { if (!keys.includes(key)) { updateObjectFromJoinedKey(nameFields, key, val) } }) const name = this.reconstructName(track.table.type, nameFields) track.editValue("name", name) } }