317 lines
9.7 KiB
JavaScript
317 lines
9.7 KiB
JavaScript
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"
|
|
}
|
|
|
|
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"
|
|
]
|
|
|
|
/**
|
|
*
|
|
* @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() {
|
|
for (const table of Object.values(this.editor.tables)) {
|
|
this.checkTableIntegrity(table)
|
|
}
|
|
|
|
this.nextError()
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @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 = channels[0]
|
|
}
|
|
|
|
break
|
|
|
|
case "subtitle":
|
|
const stLang = findLanguage(lower)
|
|
if (stLang !== null) {fields.language = stLang}
|
|
const forced = parts.includes("forced")
|
|
if (forced) {fields.flags.forced = forced}
|
|
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 === "fr") {
|
|
name += " VFF"
|
|
} else if (fields.language === "fr-ca") {
|
|
name =+ " VFQ"
|
|
}
|
|
if (fields.channels) {
|
|
name += " : " + fields.channels
|
|
}
|
|
break
|
|
case "subtitle":
|
|
if (fields.flags.forced) {
|
|
name += " Forced"
|
|
} else {
|
|
name += " Full"
|
|
}
|
|
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
|
|
}
|
|
} |