feat: add basic integrity checks + corrections
This commit is contained in:
parent
496390ef9b
commit
e7e928b25f
@ -44,18 +44,56 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<div class="popup" id="subtitle-integrity">
|
<div class="popup" id="integrity-popup">
|
||||||
<div class="container">
|
<form class="container">
|
||||||
<h3 class="title">Subtitle Track Integrity</h3>
|
<h2 class="title">Integrity Error</h2>
|
||||||
<p id="st-integrity-desc"></p>
|
<p class="description">
|
||||||
|
There seem to be a mismatch between the name of <span id="int-track-type"></span> track <span id="int-track-idx"></span> and its '<span id="int-field-name"></span>' field
|
||||||
|
</p>
|
||||||
<div class="original">
|
<div class="original">
|
||||||
<label for="st-integrity-og">Original Value</label>
|
<h3>Current values</h3>
|
||||||
<input type="text" disabled id="st-integrity-og">
|
<div class="values">
|
||||||
|
<div class="name">
|
||||||
|
<label for="int-og-name">name</label>
|
||||||
|
<input type="text" readonly id="int-og-name">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="int-og-field" id="int-og-field-name"></label>
|
||||||
|
<input type="text" readonly id="int-og-field">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="correction">
|
<div class="correction">
|
||||||
|
<h3>Correction</h3>
|
||||||
|
<div class="options">
|
||||||
|
<div class="option">
|
||||||
|
<input type="radio" value="nothing" name="int-corr-type" id="int-corr-nothing" checked>
|
||||||
|
<label for="int-corr-nothing">Do nothing</label>
|
||||||
|
</div>
|
||||||
|
<div class="option">
|
||||||
|
<input type="radio" value="name" name="int-corr-type" id="int-corr-name">
|
||||||
|
<label for="int-corr-name">Adapt name to field</label>
|
||||||
|
<div class="value">
|
||||||
|
<label for="int-corr-new-name">name</label>
|
||||||
|
<div class="arrow"></div>
|
||||||
|
<input type="text" readonly id="int-corr-new-name">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="option">
|
||||||
|
<input type="radio" value="field" name="int-corr-type" id="int-corr-field">
|
||||||
|
<label for="int-corr-field">Adapt field to name</label>
|
||||||
|
<div class="value">
|
||||||
|
<label for="int-corr-new-field" id="int-corr-new-field-name"></label>
|
||||||
|
<div class="arrow"></div>
|
||||||
|
<input type="text" readonly id="int-corr-new-field">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="buttons">
|
||||||
|
<button type="button" id="int-apply">Apply</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -33,6 +33,11 @@ table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="text"], select {
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
--size: 0.6em;
|
--size: 0.6em;
|
||||||
--pad: 0.2em;
|
--pad: 0.2em;
|
||||||
@ -67,6 +72,38 @@ input[type="checkbox"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="radio"] {
|
||||||
|
--size: 0.5em;
|
||||||
|
--pad: 0.3em;
|
||||||
|
width: calc((var(--size) + var(--pad)) * 2);
|
||||||
|
height: calc((var(--size) + var(--pad)) * 2);
|
||||||
|
border-radius: calc(var(--size) + var(--pad));
|
||||||
|
appearance: none;
|
||||||
|
background-color: #e1e1e1;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
background-color: #daf0d1;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
width: calc(var(--size) * 2);
|
||||||
|
height: calc(var(--size) * 2);
|
||||||
|
border-radius: calc(var(--size));
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: #6ee74a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.popup {
|
.popup {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@ -80,7 +117,7 @@ input[type="checkbox"] {
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 1.2em;
|
padding: 1.2em;
|
||||||
border-radius: 0.4em;
|
border-radius: 0.8em;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -88,6 +125,126 @@ input[type="checkbox"] {
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin: 0.4em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2, h3 {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.8em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.8em 1.6em;
|
||||||
|
background-color: #dfdfdf;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.4em;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e7e7e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: #d7d7d7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#integrity-popup {
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
span {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original {
|
||||||
|
.values {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2em;
|
||||||
|
padding-left: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name, .field {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4em;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.correction {
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4em;
|
||||||
|
|
||||||
|
.option {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 0.2em 0.4em;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.4em;
|
||||||
|
border: solid black 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: #6ee74a;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
background: url("/static/images/arrow.svg");
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
border: solid black 1px;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
82
editor/public/static/images/arrow.svg
Normal file
82
editor/public/static/images/arrow.svg
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
viewBox="0 0 16.933333 16.933334"
|
||||||
|
version="1.1"
|
||||||
|
id="svg857"
|
||||||
|
inkscape:version="1.3.2 (1:1.3.2+202404261509+091e20ef0f)"
|
||||||
|
sodipodi:docname="after_icon.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs851" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="6.9603479"
|
||||||
|
inkscape:cx="17.025011"
|
||||||
|
inkscape:cy="32.900654"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="true"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="1366"
|
||||||
|
inkscape:window-height="716"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="24"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid1402"
|
||||||
|
empspacing="4"
|
||||||
|
originx="0"
|
||||||
|
originy="0"
|
||||||
|
spacingy="1.0583333"
|
||||||
|
spacingx="1.0583333"
|
||||||
|
units="px"
|
||||||
|
visible="true" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata854">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Calque 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-280.06665)">
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 2.1166666,288.53332 H 14.816666"
|
||||||
|
id="path1406"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 10.583333,285.35832 4.233333,3.175 -4.233333,3.175"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:nodetypes="ccc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
@ -1,6 +1,8 @@
|
|||||||
import Editor from "./editor.mjs";
|
import Editor from "./editor.mjs";
|
||||||
|
import * as utils from "./utils.mjs"
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
const editor = new Editor()
|
const editor = new Editor()
|
||||||
window.editor = editor
|
window.editor = editor
|
||||||
|
window.utils = utils
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import TracksTable from "./tracks_table.mjs"
|
import TracksTable from "./tracks_table.mjs"
|
||||||
import IntegrityManager from "./integrity_manager.mjs"
|
import IntegrityManager from "./integrity_manager.mjs"
|
||||||
|
import { updateObjectFromJoinedKey } from "./utils.mjs"
|
||||||
|
|
||||||
export default class Editor {
|
export default class Editor {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -74,12 +75,7 @@ export default class Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editTrack(listKey, trackIdx, key, value) {
|
editTrack(listKey, trackIdx, key, value) {
|
||||||
const keyParts = key.split("/")
|
updateObjectFromJoinedKey(this.data[listKey][trackIdx], key, value)
|
||||||
let obj = this.data[listKey][trackIdx]
|
|
||||||
for (const part of keyParts.slice(0, -1)) {
|
|
||||||
obj = obj[part]
|
|
||||||
}
|
|
||||||
obj[keyParts[keyParts.length - 1]] = value
|
|
||||||
this.setDirty()
|
this.setDirty()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,29 +1,123 @@
|
|||||||
import { Track } from "./tracks_table.mjs"
|
import { Track } from "./tracks_table.mjs"
|
||||||
import { findLanguage, isLanguageAlias, flattenObj } from "./utils.mjs"
|
import { findLanguage, isLanguageTagAlias, flattenObj, updateObjectFromJoinedKey, LANGUAGES } from "./utils.mjs"
|
||||||
|
|
||||||
class Mismatch {
|
class Mismatch {
|
||||||
constructor(track, key, value) {
|
constructor(track, key, value) {
|
||||||
|
/** @type {Track} */
|
||||||
this.track = track
|
this.track = track
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
this.key = key
|
this.key = key
|
||||||
this.value = value
|
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 {
|
export default class IntegrityManager {
|
||||||
|
IGNORE_KEYS = [
|
||||||
|
"type", "channels"
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {import('./editor.mjs').default} editor
|
* @param {import('./editor.mjs').default} editor
|
||||||
*/
|
*/
|
||||||
constructor(editor) {
|
constructor(editor) {
|
||||||
this.editor = editor
|
this.editor = editor
|
||||||
|
|
||||||
|
/** @type {Mismatch[]} */
|
||||||
this.mismatches = []
|
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() {
|
checkIntegrity() {
|
||||||
for (const table of Object.values(this.editor.tables)) {
|
for (const table of Object.values(this.editor.tables)) {
|
||||||
this.checkTableIntegrity(table)
|
this.checkTableIntegrity(table)
|
||||||
}
|
}
|
||||||
console.log(this.mismatches)
|
|
||||||
|
this.nextError()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,18 +134,25 @@ export default class IntegrityManager {
|
|||||||
*
|
*
|
||||||
* @param {Track} track
|
* @param {Track} track
|
||||||
*/
|
*/
|
||||||
checkTrackIntegrity(track) {
|
checkTrackIntegrity(track, prepend=false) {
|
||||||
let fields = this.parseName(track.table.type, track.fields["name"])
|
let fields = this.parseName(track.table.type, track.fields["name"])
|
||||||
fields = flattenObj(fields)
|
fields = flattenObj(fields)
|
||||||
|
|
||||||
Object.entries(fields).map(([key, value]) => {
|
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
|
let equal = track.fields[key] === value
|
||||||
if (key === "language") {
|
if (key === "language") {
|
||||||
equal = isLanguageAlias(value, track.fields[key])
|
equal = isLanguageTagAlias(value, track.fields[key])
|
||||||
}
|
}
|
||||||
if (!equal) {
|
if (!equal) {
|
||||||
this.addMismatchField(track, key, value)
|
this.addMismatchField(track, key, value, prepend)
|
||||||
console.error(`Mismatch for field ${key}:\n- name: ${value}\n- track: ${track.fields[key]}`)
|
//console.error(`Mismatch for field ${key}:\n- name: ${value}\n- track: ${track.fields[key]}`)
|
||||||
} else {
|
} else {
|
||||||
track.fields[key] = value
|
track.fields[key] = value
|
||||||
}
|
}
|
||||||
@ -59,25 +160,158 @@ export default class IntegrityManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseName(trackType, name) {
|
parseName(trackType, name) {
|
||||||
|
/** @type {string} */
|
||||||
const lower = name.toLowerCase()
|
const lower = name.toLowerCase()
|
||||||
const parts = lower.split(/\b/)
|
const parts = lower.split(/\b/)
|
||||||
const fields = {flags: {}}
|
const fields = {flags: {}}
|
||||||
switch (trackType) {
|
switch (trackType) {
|
||||||
case "subtitle":
|
case "audio":
|
||||||
let forced = parts.includes("forced")
|
const audioLang = findLanguage(lower)
|
||||||
let lang = findLanguage(lower)
|
if (audioLang !== null) {fields.language = audioLang}
|
||||||
if (forced) {fields.flags.forced = forced}
|
const original = parts.includes("vo")
|
||||||
if (lang !== null) {fields.language = lang}
|
|
||||||
let ad = parts.includes("sdh") || parts.includes("ad")
|
|
||||||
if (ad) {fields.flags.hearing_impaired = ad}
|
|
||||||
let original = parts.includes("vo")
|
|
||||||
if (original) {fields.flags.original = original}
|
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
|
break
|
||||||
}
|
}
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
addMismatchField(track, key, value) {
|
addMismatchField(track, key, value, prepend=false) {
|
||||||
this.mismatches.push(new Mismatch(track, key, value))
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,24 +1,31 @@
|
|||||||
import { flattenObj } from "./utils.mjs"
|
import { flattenObj, getLanguageOptions } from "./utils.mjs"
|
||||||
|
|
||||||
export class Track {
|
export class Track {
|
||||||
constructor(table, idx, fields) {
|
constructor(table, idx, fields) {
|
||||||
|
/** @type {TracksTable} */
|
||||||
this.table = table
|
this.table = table
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
this.idx = idx
|
this.idx = idx
|
||||||
|
|
||||||
|
/** @type {object} */
|
||||||
this.fields = flattenObj(fields)
|
this.fields = flattenObj(fields)
|
||||||
|
|
||||||
|
this.row = null
|
||||||
}
|
}
|
||||||
|
|
||||||
makeRow() {
|
makeRow() {
|
||||||
const tr = document.createElement("tr")
|
this.row = document.createElement("tr")
|
||||||
tr.dataset.i = this.idx
|
this.row.dataset.i = this.idx
|
||||||
this.table.fields.forEach(field => {
|
this.table.fields.forEach(field => {
|
||||||
const td = tr.insertCell(-1)
|
const td = this.row.insertCell(-1)
|
||||||
const input = this.makeInput(field, this.fields[field.key], this.idx)
|
const input = this.makeInput(field, this.fields[field.key], this.idx)
|
||||||
td.appendChild(input)
|
td.appendChild(input)
|
||||||
})
|
})
|
||||||
return tr
|
return this.row
|
||||||
}
|
}
|
||||||
|
|
||||||
makeInput(field, value) {
|
makeInput(field, value, listeners=true) {
|
||||||
let input = document.createElement("input")
|
let input = document.createElement("input")
|
||||||
let getValue = () => input.value
|
let getValue = () => input.value
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
@ -39,7 +46,7 @@ export class Track {
|
|||||||
getValue = () => input.checked
|
getValue = () => input.checked
|
||||||
const hotone = this.table.CONSTRAINTS[field.key]?.type == "hotone"
|
const hotone = this.table.CONSTRAINTS[field.key]?.type == "hotone"
|
||||||
|
|
||||||
if (hotone) {
|
if (listeners && hotone) {
|
||||||
if (value) {
|
if (value) {
|
||||||
if (field.key in this.table.hotones) {
|
if (field.key in this.table.hotones) {
|
||||||
alert(`Error in metadata file: field ${field.name} is hotone but multiple tracks are enabled`)
|
alert(`Error in metadata file: field ${field.name} is hotone but multiple tracks are enabled`)
|
||||||
@ -62,11 +69,14 @@ export class Track {
|
|||||||
|
|
||||||
case "sel":
|
case "sel":
|
||||||
input = document.createElement("select")
|
input = document.createElement("select")
|
||||||
const options = this.table.OPTIONS[field.key]
|
let options = this.table.OPTIONS[field.key]
|
||||||
|
if (typeof options === "function") {
|
||||||
|
options = options()
|
||||||
|
}
|
||||||
options.forEach(option => {
|
options.forEach(option => {
|
||||||
const opt = document.createElement("option")
|
const opt = document.createElement("option")
|
||||||
opt.innerText = option
|
opt.innerText = option.display
|
||||||
opt.value = option
|
opt.value = option.value
|
||||||
input.appendChild(opt)
|
input.appendChild(opt)
|
||||||
})
|
})
|
||||||
input.value = value
|
input.value = value
|
||||||
@ -78,21 +88,34 @@ export class Track {
|
|||||||
if (this.table.CONSTRAINTS[field.key]?.type === "readonly") {
|
if (this.table.CONSTRAINTS[field.key]?.type === "readonly") {
|
||||||
input.disabled = true
|
input.disabled = true
|
||||||
}
|
}
|
||||||
input.addEventListener("change", () => {
|
if (listeners) {
|
||||||
this.editValue(field.key, getValue())
|
input.addEventListener("change", () => {
|
||||||
})
|
this.editValue(field.key, getValue())
|
||||||
|
})
|
||||||
|
}
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
editValue(key, value) {
|
editValue(key, value) {
|
||||||
this.fields[key] = value
|
this.fields[key] = value
|
||||||
this.table.editTrack(this.idx, key, value)
|
this.table.editTrack(this.idx, key, value)
|
||||||
|
|
||||||
|
const input = this.row.querySelector(`[data-key='${key}']`)
|
||||||
|
const fieldType = this.table.getFieldProps(key).type
|
||||||
|
switch (fieldType) {
|
||||||
|
case "bool":
|
||||||
|
input.checked = value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
input.value = value
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TracksTable {
|
export default class TracksTable {
|
||||||
OPTIONS = {
|
OPTIONS = {
|
||||||
"language": ["fre", "eng"]
|
"language": getLanguageOptions
|
||||||
}
|
}
|
||||||
CONSTRAINTS = {
|
CONSTRAINTS = {
|
||||||
"flags/default": {
|
"flags/default": {
|
||||||
@ -124,6 +147,10 @@ export default class TracksTable {
|
|||||||
this.hotones = {}
|
this.hotones = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFieldProps(key) {
|
||||||
|
return this.fields.find(f => f.key == key)
|
||||||
|
}
|
||||||
|
|
||||||
loadTracks(tracks) {
|
loadTracks(tracks) {
|
||||||
this.tracks = tracks.map((t, i) => new Track(this, i, t))
|
this.tracks = tracks.map((t, i) => new Track(this, i, t))
|
||||||
this.clear()
|
this.clear()
|
||||||
|
@ -18,13 +18,49 @@ export function flattenObj(obj) {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Code (Flag): https://en.wikipedia.org/wiki/Regional_indicator_symbol
|
||||||
|
// Tag: https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes
|
||||||
export const LANGUAGES = {
|
export const LANGUAGES = {
|
||||||
"fr": ["fre", "fra", "french", "francais", "français", "vf", "vff"],
|
"fre-ca": {
|
||||||
"fr-ca": ["vfq", "quebec", "québec"],
|
display: "Français CA",
|
||||||
"en": ["eng", "ang", "english", "anglais"],
|
code: "ca",
|
||||||
"de": ["deu", "ger", "german", "allemand"],
|
aliases: ["fr-ca", "vfq", "quebec", "québec", "ca", "canada"]
|
||||||
"ko": ["kor", "cor", "korean", "coreen", "coréen"],
|
},
|
||||||
"ja": ["jap", "japanese", "japonais"]
|
"fre": {
|
||||||
|
display: "Français FR",
|
||||||
|
code: "fr",
|
||||||
|
aliases: ["fr", "fre", "fra", "french", "francais", "français", "vf", "vff", "france"]
|
||||||
|
},
|
||||||
|
"eng": {
|
||||||
|
display: "English",
|
||||||
|
code: "gb",
|
||||||
|
aliases: ["en", "eng", "ang", "english", "anglais", "uk", "gb", "usa", "british", "american", "amérique", "amerique", "angleterre", "royaume-uni"]
|
||||||
|
},
|
||||||
|
"deu": {
|
||||||
|
display: "Deutsch",
|
||||||
|
code: "de",
|
||||||
|
aliases: ["de", "deu", "ger", "german", "allemand", "deutsch", "germany", "allemagne"]
|
||||||
|
},
|
||||||
|
"kor": {
|
||||||
|
display: "Korean",
|
||||||
|
code: "kr",
|
||||||
|
aliases: ["ko", "kr", "kor", "cor", "korean", "coreen", "coréen", "corée", "coree", "korea"]
|
||||||
|
},
|
||||||
|
"jpn": {
|
||||||
|
display: "Japanese",
|
||||||
|
code: "jp",
|
||||||
|
aliases: ["ja", "jp", "jap", "japanese", "japonais", "japon", "japan"]
|
||||||
|
},
|
||||||
|
"tur": {
|
||||||
|
display: "Turkish",
|
||||||
|
code: "tr",
|
||||||
|
aliases: ["tu", "tr", "tur", "tür", "turkish", "turc", "turquie"]
|
||||||
|
},
|
||||||
|
"und": {
|
||||||
|
display: "Undefined",
|
||||||
|
code: "",
|
||||||
|
aliases: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,15 +70,43 @@ export const LANGUAGES = {
|
|||||||
*/
|
*/
|
||||||
export function findLanguage(value) {
|
export function findLanguage(value) {
|
||||||
for (const lang in LANGUAGES) {
|
for (const lang in LANGUAGES) {
|
||||||
const aliases = [lang].concat(LANGUAGES[lang])
|
const aliases = LANGUAGES[lang].aliases
|
||||||
const matches = aliases.map(a => new RegExp("\\b" + a + "\\b").test(value)).some(v => v)
|
const matches = aliases.map(a => {
|
||||||
if (matches) {
|
return new RegExp("\\b" + a + "\\b").test(value)
|
||||||
|
})
|
||||||
|
if (matches.some(v => v)) {
|
||||||
return lang
|
return lang
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLanguageAlias(langKey, value) {
|
export function isLanguageTagAlias(langTag, value) {
|
||||||
return [langKey].concat(LANGUAGES[langKey]).includes(value)
|
return LANGUAGES[langTag].aliases.includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateObjectFromJoinedKey(obj, joinedKey, value) {
|
||||||
|
const keyParts = joinedKey.split("/")
|
||||||
|
for (const part of keyParts.slice(0, -1)) {
|
||||||
|
obj = obj[part]
|
||||||
|
}
|
||||||
|
obj[keyParts[keyParts.length - 1]] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} code
|
||||||
|
*/
|
||||||
|
export function makeFlag(code) {
|
||||||
|
return code.split("")
|
||||||
|
.map(c => String.fromCodePoint(
|
||||||
|
0x1f1e6 + c.codePointAt(0) - 97
|
||||||
|
))
|
||||||
|
.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLanguageOptions() {
|
||||||
|
return Object.entries(LANGUAGES).map(([tag, props]) => {
|
||||||
|
const flag = makeFlag(props.code)
|
||||||
|
return {value: tag, display: `${flag} ${props.display}`}
|
||||||
|
})
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user