diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..d9bd5b1 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,36 @@ +name: Publish Docker image + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + submodules: true + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build Docker image + run: | + repo_name=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + docker build -t ghcr.io/${repo_name}/dvb-i-tools:latest . + + - name: Push Docker image to GitHub Container Registry + run: | + repo_name=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + docker push ghcr.io/${repo_name}/dvb-i-tools:latest diff --git a/CMCD.js b/CMCD.js new file mode 100644 index 0000000..5fe0061 --- /dev/null +++ b/CMCD.js @@ -0,0 +1,134 @@ +/** + * CMCD.js + * + * Definitions and check related to CMCD + * + */ + +import { dvbi } from "./DVB-I_definitions.js"; +import { CMCD_MODE_REQUEST, CMCD_MODE_RESPONSE, CMCD_MODE_INTERVAL } from "./DVB-I_definitions.js"; +import { APPLICATION, WARNING } from "./error_list.js"; +import { isIn } from "./utils.js"; + +export const CMCD_keys = { + encoded_bitrate: "br", + buffer_length: "bl", + buffer_starvation: "bs", + buffer_starvation_duration: "bsd", + cdn_id: "cdn", + content_id: "cid", + object_duration: "d", + deadline: "dl", + live_stream_latency: "ltc", + measured_throughput: "mtp", + next_object_request: "nor", + next_range_request: "nrr", + object_type: "ot", + playback_rate: "pr", + requested_maximum_throughput: "rtp", + response_code: "rc", + streaming_format: "sf", + session_id: "sid", + interstitial: "int", + state: "sta", + stream_type: "st", + startup: "su", + time_to_first_byte: "ttfb", + time_to_first_body_byte: "ttfbb", + time_to_last_byte: "ttlb", + timestamp: "ts", + top_encoded_bitrate: "tb", + lowest_encoded_bitrate: "lb", + top_aggregated_encoded_bitrate: "tab", + lowest_aggregated_encoded_bitrate: "lab", + request_url: "url", + playhead_position: "pp", + player_error_code: "ec", + media_start_delay: "msd", + cmcd_version: "v", +}; + +const all_reporting_modes = [CMCD_MODE_REQUEST, CMCD_MODE_RESPONSE, CMCD_MODE_INTERVAL]; +const CMCDv1_keys = [ + { key: CMCD_keys.encoded_bitrate, allow_modes: all_reporting_modes }, + { key: CMCD_keys.buffer_length, allow_modes: all_reporting_modes }, + { key: CMCD_keys.buffer_starvation, allow_modes: all_reporting_modes }, + { key: CMCD_keys.content_id, allow_modes: all_reporting_modes }, + { key: CMCD_keys.object_duration, allow_modes: [CMCD_MODE_REQUEST, CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.deadline, allow_modes: [CMCD_MODE_REQUEST, CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.measured_throughput, allow_modes: all_reporting_modes }, + { key: CMCD_keys.next_object_request, allow_modes: [CMCD_MODE_REQUEST, CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.next_range_request, allow_modes: [CMCD_MODE_REQUEST, CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.object_type, allow_modes: [CMCD_MODE_REQUEST, CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.playback_rate, allow_modes: all_reporting_modes }, + { key: CMCD_keys.requested_maximum_throughput, allow_modes: [CMCD_MODE_REQUEST, CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.streaming_format, allow_modes: all_reporting_modes }, + { key: CMCD_keys.session_id, allow_modes: all_reporting_modes }, + { key: CMCD_keys.stream_type, allow_modes: all_reporting_modes }, + { key: CMCD_keys.startup, allow_modes: [CMCD_MODE_REQUEST, CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.top_encoded_bitrate, allow_modes: all_reporting_modes }, + { key: CMCD_keys.cmcd_version, allow_modes: all_reporting_modes }, +]; + +const CMCDv2_keys = CMCDv1_keys.concat([ + { key: CMCD_keys.buffer_starvation_duration, allow_modes: [CMCD_MODE_REQUEST, CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.cdn_id, allow_modes: all_reporting_modes }, + { ley: CMCD_keys.live_stream_latency, allow_modes: all_reporting_modes }, + { key: CMCD_keys.response_code, allow_modes: [CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.interstitial, allow_modes: [] }, + { key: CMCD_keys.state, allow_modes: all_reporting_modes }, + { key: CMCD_keys.time_to_first_byte, allow_modes: [CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.time_to_first_body_byte, allow_modes: [CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.time_to_last_byte, allow_modes: [CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.timestamp, allow_modes: all_reporting_modes }, + { key: CMCD_keys.lowest_encoded_bitrate, allow_modes: all_reporting_modes }, + { key: CMCD_keys.top_aggregated_encoded_bitrate, allow_modes: all_reporting_modes }, + { key: CMCD_keys.lowest_aggregated_encoded_bitrate, allow_modes: all_reporting_modes }, + { key: CMCD_keys.request_url, allow_modes: [CMCD_MODE_RESPONSE] }, + { key: CMCD_keys.playhead_position, allow_modes: all_reporting_modes }, + { key: CMCD_keys.player_error_code, allow_modes: all_reporting_modes }, + { key: CMCD_keys.media_start_delay, allow_modes: all_reporting_modes }, +]); + +const isCustomKey = (key) => key.includes("-"); +const reportingMode = (mode) => (mode.includes(":") ? mode.substring(mode.lastIndexOf(":") + 1) : "***"); + +export function checkCMCDkeys(CMCD, errs, errCode) { + if (!CMCD) { + errs.addError({ type: APPLICATION, code: `${errCode}-00`, message: "checkCMCDkeys() called with CMCD=null" }); + return; + } + const keys = CMCD.attr(dvbi.a_enabledKeys)?.value().split(" "); + const reporting_mode = CMCD.attr(dvbi.a_reportingMode).value(); + if (keys) { + keys.forEach((key) => { + const reserved_key = CMCDv1_keys.find((e) => e.key == key); + if (reserved_key) { + if (!isIn(reserved_key.allow_modes, reporting_mode)) + errs.addError({ + code: `${errCode}-01`, + message: `${key.quote()} is not allowed for the specified reporting mode (${reportingMode(reporting_mode)})`, + fragment: CMCD, + }); + } else if (isCustomKey(key)) + errs.addError({ + type: WARNING, + code: `${errCode}-02`, + message: `custom CMCD key ${key.quote()} in use`, + fragment: CMCD, + }); + else + errs.addError({ + code: `${errCode}-03`, + message: `${key.quote()} is not a reserved CMCDv1 key or the correct format for a custom key`, + fragment: CMCD, + }); + }); + } + if (reporting_mode == CMCD_MODE_INTERVAL && !isIn(keys, CMCD_keys.timestamp)) + errs.addError({ + code: `${errCode}-04`, + message: `the key ${CMCD_keys.timestamp.quote()} is required to be specified for ${reportingMode(reporting_mode)} reporting`, + fragment: CMCD, + }); +} diff --git a/DVB-I_definitions.js b/DVB-I_definitions.js index 283fab0..429d056 100644 --- a/DVB-I_definitions.js +++ b/DVB-I_definitions.js @@ -1,6 +1,6 @@ /** * DVB-I_defintions.js - * + * * Defintions made in DVB A177 Bluebooks */ import { tva, tvaEA, tvaEC, TVA_CSmetadata } from "./TVA_definitions.js"; @@ -9,7 +9,6 @@ const DVB_metadata = "urn:dvb:metadata"; const DVB_CSmetadata = `${DVB_metadata}:cs`, FVC_CSmetadata = "urn:fvc:metadata:cs"; - const PaginationPrefix = `${FVC_CSmetadata}:HowRelatedCS:2015-12:pagination`, NowNextCRIDPrefix = "crid://dvb.org/metadata/schedules/now-next"; @@ -35,6 +34,14 @@ const HbbTVStandardPrefix = "urn:hbbtv:appinformation:standardversion:hbbtv"; const HbbTVFeaturePrefix = "urn:hbbtv:appinformation:optionalfeature:hbbtv"; const CTAStandardPrefix = "urn:cta:wave:appinformation:standardversion"; +const CMCDterm = `${DVB_metadata}:cmcd`; +export const CMCD_MODE_REQUEST = `${CMCDterm}:delivery:request`, + CMCD_MODE_RESPONSE = `${CMCDterm}:delivery:response`, + CMCD_MODE_INTERVAL = `${CMCDterm}:delivery:interval`; + +export const CMCD_METHOD_HTTP_HEADER = `${CMCDterm}:delivery:customHTTPHeader`, + CMCD_METHOD_QUERY_ARGUMENT = `${CMCDterm}:delivery:queryArguments`; + export const dvbi = { A177_Namespace: `${DVB_metadata}:servicediscovery:2019`, A177r1_Namespace: `${DVB_metadata}:servicediscovery:2020`, @@ -43,6 +50,7 @@ export const dvbi = { A177r4_Namespace: `${DVB_metadata}:servicediscovery:2022b`, A177r5_Namespace: `${DVB_metadata}:servicediscovery:2023`, A177r6_Namespace: `${DVB_metadata}:servicediscovery:2024`, + A177r7_Namespace: `${DVB_metadata}:servicediscovery:2025`, ApplicationStandards: [ `${HbbTVStandardPrefix}:1.2.1`, @@ -164,6 +172,12 @@ export const dvbi = { APP_OUTSIDE_AVAILABILITY: `${LINKED_APLICATION_CS}:2`, APP_SERVICE_PROVIDER: `${LINKED_APLICATION_CS}:3`, + // A177r7 + APP_IN_SERIES: `${LINKED_APLICATION_CS}:1.3`, + APP_LIST_INSTALLATION: `${LINKED_APLICATION_CS}:4.1`, + APP_WITHDRAW_AGREEMENT: `${LINKED_APLICATION_CS}:4.2`, + APP_RENEW_AGREEMENT: `${LINKED_APLICATION_CS}:4.3`, + NVOD_MODE_REFERENCE: "reference", NVOD_MODE_TIMESHIFTED: "timeshifted", @@ -180,6 +194,7 @@ export const dvbi = { a_Address: "Address", a_CGSID: "CGSID", a_channelNumber: "channelNumber", + a_contentId: "contentId", a_contentLanguage: tva.a_contentLanguage, a_contentType: tva.a_contentType, a_controlRemoteAccessOverInternet: "controlRemoteAccessOverInternet", @@ -202,6 +217,7 @@ export const dvbi = { a_dvb_ssrc_upstream_client: "dvb-ssrc-upstream-client", a_dvb_t_wait_max: "dvb-t-wait-max", a_dvb_t_wait_min: "dvb-t-wait-min", + a_enabledKeys: "enabledKeys", a_encryptionScheme: "encryptionScheme", a_endTime: "endTime", a_extensionName: "extensionName", @@ -228,6 +244,8 @@ export const dvbi = { a_region: "region", a_regionID: "regionID", a_regulatorListFlag: "regulatorListFlag", + a_reportingMethod: "reportingMethod", + a_reportingMode: "reportingMode", a_responseStatus: "responseStatus", a_rtcp_bandwidth: "rtcp-bandwidth", a_rtcp_mux: "rtcp-mux", @@ -260,6 +278,7 @@ export const dvbi = { e_Availability: "Availability", e_CASystemId: "CASystemId", e_ChannelBonding: "ChannelBonding", + e_CMCD: "CMCD", e_CNAME: "CNAME", e_Colorimetry: "Colorimetry", e_ContentAttributes: "ContentAttributes", diff --git a/IANA_languages.js b/IANA_languages.js index 9c35d08..79eb569 100644 --- a/IANA_languages.js +++ b/IANA_languages.js @@ -1,6 +1,6 @@ /** * IANA_languages.js - * + * * Load and check language identifiers */ import { readFile, readFileSync } from "fs"; @@ -9,7 +9,7 @@ import chalk from "chalk"; import { datatypeIs } from "./phlib/phlib.js"; -import { handleErrors } from "./fetch_err_handler.js"; +import handleErrors from "./fetch_err_handler.js"; import { isIn, isIni } from "./utils.js"; import { isHTTPURL } from "./pattern_checks.js"; import fetchS from "sync-fetch"; @@ -46,6 +46,9 @@ export default class IANAlanguages { stats(res) { res.numLanguages = this.#languagesList.length; res.numRedundantLanguages = this.#redundantLanguagesList.length; + let t = []; + this.#redundantLanguagesList.forEach((e) => t.push(`${e.tag}${e.preferred ? `~${e.preferred}` : ""}`)); + res.RedundantLanguages = t.join(", "); res.numLanguageRanges = this.#languageRanges.length; res.numSignLanguages = this.#signLanguagesList.length; if (this.#languageFileDate) res.languageFileDate = this.#languageFileDate; @@ -66,9 +69,7 @@ export default class IANAlanguages { * @return {boolean} true if the language subtag is a sign language */ function isSignLanguage(items) { - for (let i = 0; i < items.length; i++) - if (items[i].startsWith("Description") && items[i].toLowerCase().includes("sign")) - return true; + for (let i = 0; i < items.length; i++) if (items[i].startsWith("Description") && items[i].toLowerCase().includes("sign")) return true; return false; } @@ -141,9 +142,8 @@ export default class IANAlanguages { } else console.log(chalk.red(`error loading languages ${err}`)); }.bind(this) ); - } - else { - let langs = readFileSync(languagesFile, { encoding: "utf-8" } ).toString(); + } else { + let langs = readFileSync(languagesFile, { encoding: "utf-8" }).toString(); this.#processLanguageData(langs); } } @@ -161,7 +161,7 @@ export default class IANAlanguages { if (purge) this.empty(); - if (async) + if (async) fetch(languagesURL) .then(handleErrors) .then((response) => response.text()) @@ -175,14 +175,13 @@ export default class IANAlanguages { console.log(chalk.red(error.message)); } if (resp) { - if (resp.ok) - this.#processLanguageData(response.text); - else console.log(chalk.red(`error (${error}) retrieving ${languagesURL}`)); + if (resp.ok) this.#processLanguageData(resp.text); + else console.log(chalk.red(`error (${resp.error}) retrieving ${languagesURL}`)); } } } - loadLanguages(options, async=true) { + loadLanguages(options, async = true) { if (!options) options = {}; if (!Object.prototype.hasOwnProperty.call(options, "purge")) options.purge = false; @@ -202,6 +201,13 @@ export default class IANAlanguages { if (datatypeIs(value, "string")) { if (this.#languageRanges.find((range) => range.start <= value && value <= range.end)) return { resp: this.languageKnown }; + let found = this.#redundantLanguagesList.find((e) => e.tag.toLowerCase() == value.toLowerCase()); + if (found) { + let res = { resp: this.languageRedundant }; + if (found?.preferred) res.pref = found.preferred; + return res; + } + if (value.indexOf("-") != -1) { let matches = true; let parts = value.split("-"); @@ -213,15 +219,6 @@ export default class IANAlanguages { if (isIni(this.#languagesList, value)) return { resp: this.languageKnown }; - let lc = value.toLowerCase(); - let found = this.#redundantLanguagesList.find((e) => e.tag.toLowerCase() == lc); - if (found) { - let res = { resp: this.languageRedundant }; - if (found?.preferred) res.pref = found.preferred; - []; - return res; - } - return { resp: this.languageUnknown }; } return { resp: this.languageInvalidType }; diff --git a/ISO_countries.js b/ISO_countries.js index 6638cc7..fff66f0 100644 --- a/ISO_countries.js +++ b/ISO_countries.js @@ -1,6 +1,6 @@ -/** +/** * ISO_countries.js - * + * * Load and check country codes */ import { readFile, readFileSync } from "fs"; @@ -8,7 +8,7 @@ import { readFile, readFileSync } from "fs"; import chalk from "chalk"; import fetchS from "sync-fetch"; -import { handleErrors } from "./fetch_err_handler.js"; +import handleErrors from "./fetch_err_handler.js"; import { isHTTPURL } from "./pattern_checks.js"; /** @@ -67,7 +67,7 @@ export default class ISOcountries { }.bind(this) ); else { - let langs = readFileSync(countriesFile, { encoding: "utf-8" } ).toString(); + let langs = readFileSync(countriesFile, { encoding: "utf-8" }).toString(); this.#countriesList = loadCountryData(langs); } } @@ -90,22 +90,21 @@ export default class ISOcountries { .then((response) => response.text()) .then((responseText) => (this.#countriesList = loadCountryData(responseText))) .catch((error) => console.log(chalk.red(`error (${error}) retrieving ${countriesURL}`))); - else { - let resp = null; - try { - resp = fetchS(countriesURL); - } catch (error) { - console.log(chalk.red(error.message)); - } - if (resp) { - if (resp.ok) - this.#countriesList = loadCountryData(response.text) - else console.log(chalk.red(`error (${error}) retrieving ${languagesURL}`)); - } + else { + let resp = null; + try { + resp = fetchS(countriesURL); + } catch (error) { + console.log(chalk.red(error.message)); } + if (resp) { + if (resp.ok) this.#countriesList = loadCountryData(response.text); + else console.log(chalk.red(`error (${error}) retrieving ${languagesURL}`)); + } + } } - loadCountries(options, async=true) { + loadCountries(options, async = true) { if (!options) options = {}; if (!options.purge) options.purge = true; diff --git a/TVA_definitions.js b/TVA_definitions.js index ce41f10..4ae776f 100644 --- a/TVA_definitions.js +++ b/TVA_definitions.js @@ -1,6 +1,6 @@ /** * TVA_defintions.js - * + * * Definitions made by TV0Anytime in versions of ETSI TS 102 822-3-1 */ @@ -210,6 +210,7 @@ export const tva = { a_purpose: "purpose", a_role: "role", a_serviceIDRef: "serviceIDRef", + a_serviceInstanceID: "serviceInstanceID", a_start: "start", a_supplemental: "supplemental", a_translation: "translation", diff --git a/accessibility_attributes_checks.js b/accessibility_attributes_checks.js index d820b9b..9be7d45 100644 --- a/accessibility_attributes_checks.js +++ b/accessibility_attributes_checks.js @@ -1,7 +1,7 @@ /** * accessibility_attribites_checks.js - * - * Checks the value space of the element against the rules and + * + * Checks the value space of the element against the rules and * values provided in DVB A177. */ import { datatypeIs } from "./phlib/phlib.js"; @@ -13,8 +13,9 @@ import { APPLICATION, WARNING } from "./error_list.js"; import { checkTopElementsAndCardinality } from "./schema_checks.js"; import { CS_URI_DELIMITER } from "./classification_scheme.js"; +import { DumpString } from "./utils.js"; -export function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, errCode) { +export default function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, errCode) { const ACCESSIBILITY_CHECK_KEY = "accessibility attributes"; if (!AccessibilityAttributes) { @@ -126,23 +127,27 @@ export function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, }; let checkCS = (elem, childName, cs, errNum, storage = null) => { - let children = elem.childNodes(); + let rc = true, + children = elem.childNodes(); if (children) children.forEachSubElement((e) => { if (e.name() == childName) { let href = e.attr(tva.a_href) ? e.attr(tva.a_href).value() : null; - if (href && !cs.isIn(href)) + if (href && !cs.isIn(href)) { errs.addError({ code: `${errCode}-${errNum}`, fragment: e, message: `"${href}" is not valid for ${e.name().elementize()} in ${elem.name().elementize()}`, key: ACCESSIBILITY_CHECK_KEY, }); + rc = false; + } if (storage && datatypeIs(storage, "array") && href) { storage.push(href); } } }); + return rc; }; let checkSignLanguage = (elem, childName, errNum) => { @@ -150,13 +155,31 @@ export function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, if (children) children.forEachSubElement((e) => { if (e.name() == childName) { - if (cs.KnownLanguages.checkSignLanguage(e.text()) != cs.KnownLanguages.languageKnown) + let languageCode = e.text(); + let lState = cs.KnownLanguages.isKnown(languageCode); + if (lState.resp == cs.KnownLanguages.languageRedundant) { errs.addError({ - code: `${errCode}-${errNum}`, + code: `${errCode}-${errNum}a`, + fragment: e, + message: `sign language ${languageCode.quote()} is redundant${lState.pref ? `, use ${lState.pref.quote()} instead` : ""}`, + key: "deprecated language", + type: WARNING, + }); + if (lState.pref) languageCode = lState.pref; + } + + if (cs.KnownLanguages.checkSignLanguage(languageCode) != cs.KnownLanguages.languageKnown) { + errs.addError({ + code: `${errCode}-${errNum}b`, fragment: e, - message: `"${e.text()}" is not a valid sign language for ${e.name().elementize()} in ${elem.name().elementize()}`, + message: `${languageCode.quote()} is not a valid sign language for ${e.name().elementize()} in ${elem.name().elementize()}`, key: ACCESSIBILITY_CHECK_KEY, }); + errs.errorDescription({ + code: `${errCode}-${errNum}b`, + description: `language used for ${e.name().elementize()}} must be a sign language in the IANA language-subtag-regostry`, + }); + } } }); }; @@ -211,9 +234,21 @@ export function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, if (children) children.forEachSubElement((e) => { if (e.name() == childName) { + checkTopElementsAndCardinality( + e, + [ + { name: tva.e_Coding, minOccurs: 0 }, + { name: tva.e_MixType, minOccurs: 0 }, + { name: tva.e_AudioLanguage, minOccurs: 0 }, + ], + tvaEC.AudioAttributes, + false, + errs, + `${errCode}-${errNum}a` + ); // AccessibilityAttributes.*.AudioAttribites.AudioLanguage - checkLanguage(e, tva.e_AudioLanguage, `${errNum}a`); - checkLanguagePurpose(e, tva.e_AudioLanguage, `${errNum}b`); + checkLanguage(e, tva.e_AudioLanguage, `${errNum}b`); + checkLanguagePurpose(e, tva.e_AudioLanguage, `${errNum}c`); let c2 = e.childNodes(); if (c2) c2.forEachSubElement((e2) => { @@ -222,7 +257,7 @@ export function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, // AccessibilityAttributes.*.AudioAttribites.Coding if (e2.attr(tva.a_href) && !cs.AudioCodecCS.isIn(e2.attr(tva.a_href).value())) errs.addError({ - code: `${errCode}-${errNum}c`, + code: `${errCode}-${errNum}d`, fragment: e2, message: `"${e2.attr(tva.a_href).value()}" not not valid for ${elem.name().elementize()}${e.name().elementize()}${e2.name().elementize()}`, key: ACCESSIBILITY_CHECK_KEY, @@ -232,7 +267,7 @@ export function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, // AccessibilityAttributes.*.AudioAttribites.MixType if (e2.attr(tva.a_href) && !cs.AudioPresentationCS.isIn(e2.attr(tva.a_href).value())) errs.addError({ - code: `${errCode}-${errNum}d`, + code: `${errCode}-${errNum}e`, fragment: e2, message: `"${e2.attr(tva.a_href).value()}" not not valid for ${elem.name().elementize()}${e.name().elementize()}${e2.name().elementize()}`, key: ACCESSIBILITY_CHECK_KEY, @@ -323,7 +358,8 @@ export function CheckAccessibilityAttributes(AccessibilityAttributes, cs, errs, `${errCode}-71` ); checkAppInformation(elem, 72); - checkCS(elem, tva.e_Coding, cs.VideoCodecCS, 73); + if (!checkCS(elem, tva.e_Coding, cs.VideoCodecCS, 73)) + errs.errorDescription({ code: `${errCode}-73`, description: `value for ${tva.e_Coding.elementize()} is not taken from the VideoCodecCS` }); checkSignLanguage(elem, tva.e_SignLanguage, 74); break; case tva.e_DialogueEnhancementAttributes: diff --git a/cg_check.js b/cg_check.js index 405a853..1c22e4a 100644 --- a/cg_check.js +++ b/cg_check.js @@ -1,6 +1,6 @@ /** * cg_check.js - * + * * Validate content guide metadata */ import process from "process"; @@ -25,7 +25,7 @@ import { ValidatePromotionalStillImage } from "./related_material_checks.js"; import { cg_InvalidHrefValue, NoChildElement, keys } from "./common_errors.js"; import { checkAttributes, checkTopElementsAndCardinality, hasChild, SchemaCheck, SchemaLoad, SchemaVersionCheck } from "./schema_checks.js"; import { checkLanguage, GetNodeLanguage, checkXMLLangs } from "./multilingual_element.js"; -import { writeOut } from "./validator.js"; +import writeOut from "./logger.js"; import { CURRENT, OLD } from "./globals.js"; import { LoadGenres, @@ -42,7 +42,7 @@ import { LoadCountries, } from "./classification_scheme_loaders.js"; import { LoadCredits } from "./role_loader.js"; -import { CheckAccessibilityAttributes } from "./accessibility_attributes_checks.js"; +import CheckAccessibilityAttributes from "./accessibility_attributes_checks.js"; // convenience/readability values const DEFAULT_LANGUAGE = "***"; @@ -277,7 +277,7 @@ export default class ContentGuideCheck { #subtitleCodings; #subtitlePurposes; - constructor(useURLs, opts, async=true) { + constructor(useURLs, opts, async = true) { this.#numRequests = 0; this.supportedRequests = supportedRequests; @@ -664,7 +664,7 @@ export default class ContentGuideCheck { case tva.e_MinimumAge: checkAttributes(pgChild, [], [], tvaEA.MinimumAge, errs, `${errCode}-10`); if (thisCountry.MinimumAge) { - // only one minimum age value is premitted per country + // only one minimum age value is permitted per country errs.addError({ code: `${errCode}-11`, key: keys.k_ParentalGuidance, @@ -675,6 +675,14 @@ export default class ContentGuideCheck { }); } thisCountry.MinimumAge = pgChild; + let age = pgChild.text().parseInt(); + if ((age < 4 || age > 18) && age != 255) + errs.addError({ + code: `${errCode}-12`, + key: keys.k_ParentalGuidance, + message: `value of ${tva.e_MinimumAge.elementize()} must be between 4 and 18 (to align with parental_rating_descriptor) or be 255`, + fragment: pgChild, + }); break; case tva.e_ParentalRating: checkAttributes(pgChild, [tva.a_href], [], tvaEA.ParentalRating, errs, `${errCode}-20`); @@ -1274,12 +1282,14 @@ export default class ContentGuideCheck { const titleLang = GetNodeLanguage(Title, false, errs, `${errCode}-2`, this.#knownLanguages); const titleStr = unEntity(Title.text()); - if (titleStr.length > dvbi.MAX_TITLE_LENGTH) + if (titleStr.length > dvbi.MAX_TITLE_LENGTH) { errs.addError({ code: `${errCode}-11`, message: `${tva.e_Title.elementize()} length exceeds ${dvbi.MAX_TITLE_LENGTH} characters`, fragment: Title, }); + errs.errorDescription({ code: `${errCode}-11`, description: "refer clause 6.10.5 in A177" }); + } switch (titleType) { case mpeg7.TITLE_TYPE_MAIN: if (mainTitles.find((e) => e.lang == titleLang)) @@ -1310,6 +1320,8 @@ export default class ContentGuideCheck { message: `${tva.a_type.attribute()} must be ${mpeg7.TITLE_TYPE_MAIN.quote()} or ${mpeg7.TITLE_TYPE_SECONDARY.quote()} for ${tva.e_Title.elementize()}`, fragment: Title, }); + errs.errorDescription({ code: `${errCode}-15`, description: "refer to the relevant subsection of clause 6.10.5 in A177" }); + break; } } secondaryTitles.forEach((item) => { @@ -1522,6 +1534,11 @@ export default class ContentGuideCheck { } } + /*private*/ #NotCRIDFormat(errs, error) { + errs.addError(error); + errs.errorDescription({ code: error?.code, description: "format if a CRID is defined in clause 8 of ETSI TS 102 822" }); + } + /** * validate the element against the profile for the given request/response type * @@ -1573,7 +1590,7 @@ export default class ContentGuideCheck { if (ProgramInformation.attr(tva.a_programId)) { programCRID = ProgramInformation.attr(tva.a_programId).value(); if (!isCRIDURI(programCRID)) - errs.addError({ + this.#NotCRIDFormat(errs, { code: "PI011", message: `${tva.a_programId.attribute(ProgramInformation.name())} is not a valid CRID (${programCRID})`, line: ProgramInformation.line(), @@ -1617,7 +1634,7 @@ export default class ContentGuideCheck { fragment: child, }); else if (!isCRIDURI(foundCRID)) - errs.addError({ + this.#NotCRIDFormat(errs, { code: "PI033", message: `${tva.a_crid.attribute(`${ProgramInformation.name()}.${tva.e_EpisodeOf}`)}=${foundCRID.quote()} is not a valid CRID`, fragment: child, @@ -1652,7 +1669,7 @@ export default class ContentGuideCheck { fragment: child, }); else if (!isCRIDURI(foundCRID)) - errs.addError({ + this.#NotCRIDFormat(errs, { code: "PI045", message: `${tva.a_crid.attribute(`${ProgramInformation.name()}.${tva.e_MemberOf}`)}=${foundCRID.quote()} is not a valid CRID`, fragment: child, @@ -1951,7 +1968,7 @@ export default class ContentGuideCheck { if (GroupInformation.attr(tva.a_groupId)) { const groupId = GroupInformation.attr(tva.a_groupId).value(); if (!isCRIDURI(groupId)) - errs.addError({ + this.#NotCRIDFormat(errs, { code: "GIM003", message: `${tva.a_groupId.attribute(GroupInformation.name())} value ${groupId.quote()} is not a valid CRID`, line: GroupInformation.line(), @@ -2475,6 +2492,15 @@ export default class ContentGuideCheck { * @param {Class} errs errors found in validaton */ /* private */ #ValidateInstanceDescription(props, VerifyType, InstanceDescription, isCurrentProgram, errs) { + if (!InstanceDescription) { + errs.addError({ + type: APPLICATION, + code: "ID000", + message: "ValidateInstanceDescription() called with InstanceDescription==null", + }); + return; + } + function checkGenre(genre, errs, errcode) { if (!genre) return null; checkAttributes(genre, [tva.a_href], [tva.a_type], tvaEA.Genre, errs, `${errcode}-1`); @@ -2485,19 +2511,9 @@ export default class ContentGuideCheck { message: `${tva.a_type.attribute(`${genre.parent().name()}.${+genre.name()}`)} must contain ${tva.GENRE_TYPE_OTHER.quote()}`, fragment: genre, }); - return genre.attr(tva.a_href) ? genre.attr(tva.a_href).value() : null; } - if (!InstanceDescription) { - errs.addError({ - type: APPLICATION, - code: "ID000", - message: "ValidateInstanceDescription() called with InstanceDescription==null", - }); - return; - } - let isMediaAvailability = (str) => [dvbi.MEDIA_AVAILABLE, dvbi.MEDIA_UNAVAILABLE].includes(str); let isEPGAvailability = (str) => [dvbi.FORWARD_EPG_AVAILABLE, dvbi.FORWARD_EPG_UNAVAILABLE].includes(str); let isAvailability = (str) => isMediaAvailability(str) || isEPGAvailability(str); @@ -2548,6 +2564,15 @@ export default class ContentGuideCheck { }); } + // @serviceInstanceId + if (InstanceDescription.attr(tva.a_serviceInstanceID) && InstanceDescription.attr(tva.a_serviceInstanceID).value().length == 0) + errs.addError({ + code: "ID009", + message: `${tva.a_serviceInstanceID.attribute()} should not be empty is specified`, + line: InstanceDescription.line(), + key: "empty ID", + }); + let restartGenre = null, restartRelatedMaterial = null; // @@ -2706,15 +2731,7 @@ export default class ContentGuideCheck { errs.addError({ type: APPLICATION, code: "PA000a", message: "CheckPlayerApplication() called with node==null" }); return; } - if (!Array.isArray(allowedContentTypes)) { - errs.addError({ - type: APPLICATION, - code: "PA000b", - message: "CheckPlayerApplication() called with incorrect type for allowedContentTypes", - }); - return; - } - + let allowedTypes = Array.isArray(allowedContentTypes) ? allowedContentTypes : [].concat(allowedContentTypes); if (!node.attr(tva.a_contentType)) { errs.addError({ code: `${errcode}-1`, @@ -2725,20 +2742,20 @@ export default class ContentGuideCheck { return; } - if (allowedContentTypes.includes(node.attr(tva.a_contentType).value())) { + if (allowedTypes.includes(node.attr(tva.a_contentType).value())) { switch (node.attr(tva.a_contentType).value()) { case dvbi.XML_AIT_CONTENT_TYPE: if (!isHTTPURL(node.text())) errs.addError({ code: `${errcode}-2`, - message: `${node.name().elementize()}=${node.text().quote()} is not a valid AIT URL`, + message: `${node.name().elementize()}=${node.text().quote()} is not a valid HTTP or HTTP URL`, key: keys.k_InvalidURL, fragment: node, }); break; /* case dvbi.HTML5_APP: case dvbi.XHTML_APP: - if (!patterns.isHTTPURL(node.text())) + if (!isHTTPURL(node.text())) errs.addError({code:`${errcode}-3`, message:`${node.name().elementize()}=${node.text().quote()} is not a valid URL`, key:"invalid URL", fragment:node}); break; */ @@ -2873,7 +2890,7 @@ export default class ContentGuideCheck { // let ProgramURL = OnDemandProgram.get(xPath(props.prefix, tva.e_ProgramURL), props.schema); - if (ProgramURL) this.#CheckPlayerApplication(ProgramURL, [dvbi.XML_AIT_CONTENT_TYPE], errs, "OD020"); + if (ProgramURL) this.#CheckPlayerApplication(ProgramURL, dvbi.XML_AIT_CONTENT_TYPE, errs, "OD020"); // let AuxiliaryURL = OnDemandProgram.get(xPath(props.prefix, tva.e_AuxiliaryURL), props.schema); @@ -2899,6 +2916,7 @@ export default class ContentGuideCheck { code: "OD062", message: `${tva.e_StartOfAvailability.elementize()} must be earlier than ${tva.e_EndOfAvailability.elementize()}`, multiElementError: [soa, eoa], + tag: "bad timing", }); } @@ -2914,9 +2932,8 @@ export default class ContentGuideCheck { } // - let fr = 0, - Free; - while ((Free = OnDemandProgram.get(xPath(props.prefix, tva.e_Free, ++fr), props.schema)) != null) TrueValue(Free, tva.a_value, "OD080", errs); + let Free = OnDemandProgram.get(xPath(props.prefix, tva.e_Free), props.schema); + if (Free) TrueValue(Free, tva.a_value, "OD080", errs); } /** @@ -2953,7 +2970,7 @@ export default class ContentGuideCheck { [ { name: tva.e_Program }, { name: tva.e_ProgramURL, minOccurs: 0 }, - { name: tva.e_InstanceDescription, minOccurs: 0 }, + { name: tva.e_InstanceDescription, minOccurs: 0, maxOccurs: Infinity }, { name: tva.e_PublishedStartTime }, { name: tva.e_PublishedDuration }, { name: tva.e_ActualStartTime, minOccurs: 0 }, @@ -2974,12 +2991,13 @@ export default class ContentGuideCheck { let ProgramCRID = Program.attr(tva.a_crid); if (ProgramCRID) { - if (!isCRIDURI(ProgramCRID.value())) - errs.addError({ + if (!isCRIDURI(ProgramCRID.value())) { + this.#NotCRIDFormat(errs, { code: "SE011", message: `${tva.a_crid.attribute(tva.e_Program)} is not a valid CRID (${ProgramCRID.value()})`, fragment: Program, }); + } if (!isIni(programCRIDs, ProgramCRID.value())) errs.addError({ code: "SE012", @@ -3002,8 +3020,21 @@ export default class ContentGuideCheck { }); // - let InstanceDescription = ScheduleEvent.get(xPath(props.prefix, tva.e_InstanceDescription), props.schema); - if (InstanceDescription) this.#ValidateInstanceDescription(props, tva.e_ScheduleEvent, InstanceDescription, isCurrentProgram, errs); + let id = 0, + thisInstanceDescription, + serviceIDs = []; + while ((thisInstanceDescription = ScheduleEvent.get(xPath(props.prefix, tva.e_InstanceDescription, ++id), props.schema)) != null) { + this.#ValidateInstanceDescription(props, tva.e_ScheduleEvent, thisInstanceDescription, isCurrentProgram, errs); + let instanceServiceID = thisInstanceDescription.attr(tva.a_serviceInstanceID) ? thisInstanceDescription.attr(tva.a_serviceInstanceID).value() : "dflt"; + if (isIn(serviceIDs, instanceServiceID)) + errs.addError({ + code: instanceServiceID == "dflt" ? "SE031" : "SE032", + line: thisInstanceDescription.line(), + message: instanceServiceID == "dflt" ? "Default instance description is already specified" : `Instance description for ${instanceServiceID} is already specidied`, + tag: "dulicate instance", + }); + else serviceIDs.push(instanceServiceID); + } // and let pstElem = ScheduleEvent.get(xPath(props.prefix, tva.e_PublishedStartTime), props.schema); diff --git a/classification_scheme.js b/classification_scheme.js index 789eafb..c74db67 100644 --- a/classification_scheme.js +++ b/classification_scheme.js @@ -11,7 +11,7 @@ import { parseXmlString } from "libxmljs2"; import fetchS from "sync-fetch"; import { dvb } from "./DVB_definitions.js"; -import { handleErrors } from "./fetch_err_handler.js"; +import handleErrors from "./fetch_err_handler.js"; import { hasChild } from "./schema_checks.js"; import { isHTTPURL } from "./pattern_checks.js"; @@ -89,7 +89,7 @@ export default class ClassificationScheme { * @param {String} csURL URL to the classification scheme */ - #loadFromURL(csURL, async=true) { + #loadFromURL(csURL, async = true) { const isHTTPurl = isHTTPURL(csURL); console.log(chalk.yellow(`${isHTTPurl ? "" : "--> NOT "}retrieving CS (${this.#leafsOnly ? "leaf" : "all"} nodes) from ${csURL} via fetch()`)); if (!isHTTPurl) return; @@ -120,8 +120,7 @@ export default class ClassificationScheme { this.insertValue(e, true); }); this.#schemes.push(CStext.uri); - } - else console.log(chalk.red(`error (${resp.status}:${resp.statusText}) handling ${ref}`)); + } else console.log(chalk.red(`error (${resp.status}:${resp.statusText}) handling ${ref}`)); } } } @@ -131,7 +130,7 @@ export default class ClassificationScheme { * * @param {String} classificationScheme the filename of the classification scheme */ - #loadFromFile(classificationScheme, async=true) { + #loadFromFile(classificationScheme, async = true) { console.log(chalk.yellow(`reading CS (${this.#leafsOnly ? "leaf" : "all"} nodes) from ${classificationScheme}`)); if (async) @@ -155,7 +154,7 @@ export default class ClassificationScheme { } } - loadCS(options, async=true) { + loadCS(options, async = true) { if (!options) options = {}; if (!Object.prototype.hasOwnProperty.call(options, "leafNodesOnly")) options.leafNodesOnly = false; this.#leafsOnly = options.leafNodesOnly; @@ -193,5 +192,4 @@ export default class ClassificationScheme { if (prefix == "" || node.getValue().beginsWith(prefix)) console.log(node.getValue()); }); } - } diff --git a/classification_scheme_loaders.js b/classification_scheme_loaders.js index a81ed5f..31533f2 100644 --- a/classification_scheme_loaders.js +++ b/classification_scheme_loaders.js @@ -34,15 +34,14 @@ import { } from "./data_locations.js"; import ClassificationScheme from "./classification_scheme.js"; - -export function LoadCountries(useURLs, async=true) { +export function LoadCountries(useURLs, async = true) { console.log(chalk.yellow.underline("loading countries...")); let c = new ISOcountries(false, true); c.loadCountries(useURLs ? { url: ISO3166.url } : { file: ISO3166.file }, async); return c; } -export function LoadLanguages(useURLs, async=true) { +export function LoadLanguages(useURLs, async = true) { console.log(chalk.yellow.underline("loading languages...")); let l = new IANAlanguages(); l.loadLanguages( @@ -60,7 +59,7 @@ export function LoadLanguages(useURLs, async=true) { return l; } -export function LoadVideoCodecCS(useURLs, async=true) { +export function LoadVideoCodecCS(useURLs, async = true) { console.log(chalk.yellow.underline("loading Video Codecs...")); let cs = new ClassificationScheme(); cs.loadCS( @@ -78,17 +77,17 @@ export function LoadVideoCodecCS(useURLs, async=true) { return cs; } -export function LoadAudioCodecCS(useURLs, async=true) { +export function LoadAudioCodecCS(useURLs, async = true) { console.log(chalk.yellow.underline("loading Audio Codecs...")); let cs = new ClassificationScheme(); cs.loadCS( useURLs ? { - urls: [DVB_AudioCodecCS.y2007.url, DVB_AudioCodecCS.y2020.url, MPEG7_AudioCodingFormatCS.url], + urls: [DVB_AudioCodecCS.y2007.url, DVB_AudioCodecCS.y2020.url, DVB_AudioCodecCS.y2024.url, MPEG7_AudioCodingFormatCS.url], leafNodesOnly: true, } : { - files: [DVB_AudioCodecCS.y2007.file, DVB_AudioCodecCS.y2020.file, MPEG7_AudioCodingFormatCS.file], + files: [DVB_AudioCodecCS.y2007.file, DVB_AudioCodecCS.y2020.file, DVB_AudioCodecCS.y2024.file, MPEG7_AudioCodingFormatCS.file], leafNodesOnly: true, }, async @@ -96,7 +95,7 @@ export function LoadAudioCodecCS(useURLs, async=true) { return cs; } -export function LoadGenres(useURLs, async=true) { +export function LoadGenres(useURLs, async = true) { console.log(chalk.yellow.underline("loading Genres ...")); let cs = new ClassificationScheme(); cs.loadCS( @@ -112,59 +111,59 @@ export function LoadGenres(useURLs, async=true) { return cs; } -export function LoadAccessibilityPurpose(useURLs, async=true) { +export function LoadAccessibilityPurpose(useURLs, async = true) { console.log(chalk.yellow.underline("loading Accessibility Purposes...")); let cs = new ClassificationScheme(); cs.loadCS(useURLs ? { url: TVA_AccessibilityPurposeCS.url, leafNodesOnly: true } : { file: TVA_AccessibilityPurposeCS.file, leafNodesOnly: true }, async); return cs; } -export function LoadAudioPurpose(useURLs, async=true) { +export function LoadAudioPurpose(useURLs, async = true) { console.log(chalk.yellow.underline("loading Audio Purposes...")); let cs = new ClassificationScheme(); cs.loadCS(useURLs ? { url: TVA_AudioPurposeCS.url, leafNodesOnly: true } : { file: TVA_AudioPurposeCS.file, leafNodesOnly: true }, async); return cs; } -export function LoadSubtitleCarriages(useURLs, async=true) { +export function LoadSubtitleCarriages(useURLs, async = true) { console.log(chalk.yellow.underline("loading Subtitle Carriages...")); let cs = new ClassificationScheme(); cs.loadCS(useURLs ? { url: TVA_SubitleCarriageCS.url } : { file: TVA_SubitleCarriageCS.file }, async); return cs; } -export function LoadSubtitleCodings(useURLs, async=true) { +export function LoadSubtitleCodings(useURLs, async = true) { console.log(chalk.yellow.underline("loading Subtitle Codings...")); let cs = new ClassificationScheme(); cs.loadCS(useURLs ? { url: TVA_SubitleCodingFormatCS.url } : { file: TVA_SubitleCodingFormatCS.file }, async); return cs; } -export function LoadSubtitlePurposes(useURLs, async=true) { +export function LoadSubtitlePurposes(useURLs, async = true) { console.log(chalk.yellow.underline("loading Subtitle Purposes...")); let cs = new ClassificationScheme(); cs.loadCS(useURLs ? { url: TVA_SubitlePurposeCS.url } : { file: TVA_SubitlePurposeCS.file }, async); return cs; } -export function LoadAudioConformanceCS(useURLs, async=true) { +export function LoadAudioConformanceCS(useURLs, async = true) { console.log(chalk.yellow.underline("loading Audio Conformance Points...")); let cs = new ClassificationScheme(); cs.loadCS(useURLs ? { url: DVB_AudioConformanceCS.url, leafNodesOnly: true } : { file: DVB_AudioConformanceCS.file, leafNodesOnly: true }, async); return cs; } -export function LoadVideoConformanceCS(useURLs, async=true) { +export function LoadVideoConformanceCS(useURLs, async = true) { console.log(chalk.yellow.underline("loading Video Conformance Points...")); let cs = new ClassificationScheme(); cs.loadCS( useURLs ? { - urls: [DVB_VideoConformanceCS.y2017.url, DVB_VideoConformanceCS.y2021.url, DVB_VideoConformanceCS.y2022.url], + urls: [DVB_VideoConformanceCS.y2017.url, DVB_VideoConformanceCS.y2021.url, DVB_VideoConformanceCS.y2022.url, DVB_VideoConformanceCS.y2024.url], leafNodesOnly: true, } : { - files: [DVB_VideoConformanceCS.y2017.file, DVB_VideoConformanceCS.y2021.file, DVB_VideoConformanceCS.y2022.file], + files: [DVB_VideoConformanceCS.y2017.file, DVB_VideoConformanceCS.y2021.file, DVB_VideoConformanceCS.y2022.file, DVB_VideoConformanceCS.y2024.file], leafNodesOnly: true, }, async @@ -172,36 +171,35 @@ export function LoadVideoConformanceCS(useURLs, async=true) { return cs; } -export function LoadAudioPresentationCS(useURLs, async=true) { +export function LoadAudioPresentationCS(useURLs, async = true) { console.log(chalk.yellow.underline("loading AudioPresentation...")); let cs = new ClassificationScheme(); cs.loadCS(useURLs ? { url: MPEG7_AudioPresentationCS.url } : { file: MPEG7_AudioPresentationCS.file }, async); return cs; } -export function LoadRecordingInfoCS(useURLs, async=true) { +export function LoadRecordingInfoCS(useURLs, async = true) { console.log(chalk.yellow.underline("loading Recording Info...")); let cs = new ClassificationScheme(); cs.loadCS(useURLs ? { url: DVBI_RecordingInfoCS.url } : { file: DVBI_RecordingInfoCS.file }, async); return cs; } - -export function LoadPictureFormatCS(useURLs, async=true) { +export function LoadPictureFormatCS(useURLs, async = true) { console.log(chalk.yellow.underline("loading PictureFormats...")); let cs = new ClassificationScheme(); cs.loadCS(useURLs ? { url: TVA_PictureFormatCS.url } : { file: TVA_PictureFormatCS.file }, async); return cs; } -export function LoadColorimetryCS(useURLs, async=true) { +export function LoadColorimetryCS(useURLs, async = true) { console.log(chalk.yellow.underline("loading Colorimetry...")); let cs = new ClassificationScheme(); cs.loadCS(useURLs ? { url: DVB_ColorimetryCS.y2020.url, leafNodesOnly: true } : { file: DVB_ColorimetryCS.y2020.file, leafNodesOnly: true }, async); return cs; } -export function LoadServiceTypeCS(useURLs, async=true) { +export function LoadServiceTypeCS(useURLs, async = true) { console.log(chalk.yellow.underline("loading ServiceTypes...")); let cs = new ClassificationScheme(); cs.loadCS(useURLs ? { url: DVBI_ServiceTypeCS.url } : { file: DVBI_ServiceTypeCS.file }, async); diff --git a/data_locations.js b/data_locations.js index 0f86386..421a934 100644 --- a/data_locations.js +++ b/data_locations.js @@ -1,23 +1,30 @@ -/** +/** * data_locations.js - * + * * paths and URLs to various files used by the validation toole */ import { join, dirname } from "path"; import { fileURLToPath } from "url"; -export const __dirname =dirname(fileURLToPath(import.meta.url)); +export const __dirname = dirname(fileURLToPath(import.meta.url)); export const __dirname_linux = __dirname.replace(/\\/g, "/"); const REPO_RAW = "https://raw.githubusercontent.com/paulhiggs/dvb-i-tools/main/"; const DVB_METADATA = "https://dvb.org/metadata/"; -const pathDVBCS = join(__dirname, "dvb/cs"), - pathDVBI = join(__dirname, "dvbi"), - pathIANA = join(__dirname, "iana"), - pathISO = join(__dirname, "iso"), - pathMPEG7 = join(__dirname, "mpeg7"), - pathTVA = join(__dirname, "tva"); +const subdirDVBCS = "dvb/cs", + subdirDVBI = "dvbi", + subdirIANA = "iana", + subdirISO = "iso", + subdirMPEG7 = "mpeg7", + subdirTVA = "tva"; + +const pathDVBCS = join(__dirname, subdirDVBCS), + pathDVBI = join(__dirname, subdirDVBI), + pathIANA = join(__dirname, subdirIANA), + pathISO = join(__dirname, subdirISO), + pathMPEG7 = join(__dirname, subdirMPEG7), + pathTVA = join(__dirname, subdirTVA); const path2007CS = join(pathDVBCS, "2007"), url2007CS = "cs/2007", @@ -30,39 +37,42 @@ const path2007CS = join(pathDVBCS, "2007"), path2021CS = join(pathDVBCS, "2021"), url2021CS = "cs/2021", path2022CS = join(pathDVBCS, "2022"), - url2022CS = "cs/2022"; + url2022CS = "cs/2022", + path2024CS = join(pathDVBCS, "2024"), + url2024CS = "cs/2024"; // SLEPR == Service List Entry Point Registry -const SLEPR_Dir = join(__dirname, "registries"), +const SLEPR_subDir = "registries", + SLEPR_Dir = join(__dirname, SLEPR_subDir), SLEPR_File = "slepr-main.xml"; -export const Default_SLEPR = { file: join(SLEPR_Dir, SLEPR_File), url: `${REPO_RAW}${SLEPR_Dir}/${SLEPR_File}` }; +export const Default_SLEPR = { file: join(SLEPR_Dir, SLEPR_File), url: `${REPO_RAW}${SLEPR_subDir}/${SLEPR_File}` }; const idTVA_ContentCS = "ContentCS.xml"; -export const TVA_ContentCS = { file: join(pathTVA, idTVA_ContentCS), url: `${REPO_RAW}${pathTVA}/${idTVA_ContentCS}` }; +export const TVA_ContentCS = { file: join(pathTVA, idTVA_ContentCS), url: `${REPO_RAW}${subdirTVA}/${idTVA_ContentCS}` }; const idTVA_FormatCS = "FormatCS.xml"; -export const TVA_FormatCS = { file: join(pathTVA, idTVA_FormatCS), url: `${REPO_RAW}${pathTVA}/${idTVA_FormatCS}` }; +export const TVA_FormatCS = { file: join(pathTVA, idTVA_FormatCS), url: `${REPO_RAW}${subdirTVA}/${idTVA_FormatCS}` }; const idTVA_PictureCS = "PictureFormatCS.xml"; -export const TVA_PictureFormatCS = { file: join(pathTVA, idTVA_PictureCS), url: `${REPO_RAW}${pathTVA}/${idTVA_PictureCS}` }; +export const TVA_PictureFormatCS = { file: join(pathTVA, idTVA_PictureCS), url: `${REPO_RAW}${subdirTVA}/${idTVA_PictureCS}` }; const idTVA_ContentAlertCS = "ContentAlertCS.xml"; -export const TVA_ContentAlertCS = { file: join(pathTVA, idTVA_ContentAlertCS), url: `${REPO_RAW}${pathTVA}/${idTVA_ContentAlertCS}` }; +export const TVA_ContentAlertCS = { file: join(pathTVA, idTVA_ContentAlertCS), url: `${REPO_RAW}${subdirTVA}/${idTVA_ContentAlertCS}` }; const idTVA_AccessibilityPurposeCS = "AccessibilityPurposeCS.xml"; -export const TVA_AccessibilityPurposeCS = { file: join(pathTVA, idTVA_AccessibilityPurposeCS), url: `${REPO_RAW}${pathTVA}/${idTVA_AccessibilityPurposeCS}` }; +export const TVA_AccessibilityPurposeCS = { file: join(pathTVA, idTVA_AccessibilityPurposeCS), url: `${REPO_RAW}${subdirTVA}/${idTVA_AccessibilityPurposeCS}` }; const idTVA_AudioPurposeCS = "AudioPurposeCS.xml"; -export const TVA_AudioPurposeCS = { file: join(pathTVA, idTVA_AudioPurposeCS), url: `${REPO_RAW}${pathTVA}/${idTVA_AudioPurposeCS}` }; +export const TVA_AudioPurposeCS = { file: join(pathTVA, idTVA_AudioPurposeCS), url: `${REPO_RAW}${subdirTVA}/${idTVA_AudioPurposeCS}` }; const idTVA_SubtitleCarriageCS = "SubtitleCarriageCS.xml"; -export const TVA_SubitleCarriageCS = { file: join(pathTVA, idTVA_SubtitleCarriageCS), url: `${REPO_RAW}${pathTVA}/${idTVA_SubtitleCarriageCS}` }; +export const TVA_SubitleCarriageCS = { file: join(pathTVA, idTVA_SubtitleCarriageCS), url: `${REPO_RAW}${subdirTVA}/${idTVA_SubtitleCarriageCS}` }; const idTVA_SubtitleCodingFormatCS = "SubtitleCodingFormatCS.xml"; -export const TVA_SubitleCodingFormatCS = { file: join(pathTVA, idTVA_SubtitleCodingFormatCS), url: `${REPO_RAW}${pathTVA}/${idTVA_SubtitleCodingFormatCS}` }; +export const TVA_SubitleCodingFormatCS = { file: join(pathTVA, idTVA_SubtitleCodingFormatCS), url: `${REPO_RAW}${subdirTVA}/${idTVA_SubtitleCodingFormatCS}` }; const idTVA_SubtitlePurposeCS = "SubtitlePurposeCS.xml"; -export const TVA_SubitlePurposeCS = { file: join(pathTVA, idTVA_SubtitlePurposeCS), url: `${REPO_RAW}${pathTVA}/${idTVA_SubtitlePurposeCS}` }; +export const TVA_SubitlePurposeCS = { file: join(pathTVA, idTVA_SubtitlePurposeCS), url: `${REPO_RAW}${subdirTVA}/${idTVA_SubtitlePurposeCS}` }; const idDVB_ContentSubjectCS = "DVBContentSubjectCS-2019.xml"; export const DVBI_ContentSubject = { file: join(pathDVBI, idDVB_ContentSubjectCS), url: `${DVB_METADATA}${url2019CS}/${idDVB_ContentSubjectCS}` }; @@ -74,6 +84,7 @@ const idDVB_AudioCodecCS = "AudioCodecCS.xml"; export const DVB_AudioCodecCS = { y2007: { file: join(path2007CS, idDVB_AudioCodecCS), url: `${DVB_METADATA}${url2007CS}/${idDVB_AudioCodecCS}` }, y2020: { file: join(path2020CS, idDVB_AudioCodecCS), url: `${DVB_METADATA}${url2020CS}/${idDVB_AudioCodecCS}` }, + y2024: { file: join(path2024CS, idDVB_AudioCodecCS), url: `${REPO_RAW}${url2024CS}/${idDVB_AudioCodecCS}` /*`${DVB_METADATA}${url2024CS}/${idDVB_AudioCodecCS}`*/ }, }; const idDVB_VideoCodecCS = "VideoCodecCS.xml"; @@ -89,13 +100,13 @@ export const DVB_ColorimetryCS = { }; const idMPEG_AudioCodingFormatCS = "AudioCodingFormatCS.xml"; -export const MPEG7_AudioCodingFormatCS = { file: join(pathMPEG7, idMPEG_AudioCodingFormatCS), url: `${REPO_RAW}${pathMPEG7}/${idMPEG_AudioCodingFormatCS}` }; +export const MPEG7_AudioCodingFormatCS = { file: join(pathMPEG7, idMPEG_AudioCodingFormatCS), url: `${REPO_RAW}${subdirMPEG7}/${idMPEG_AudioCodingFormatCS}` }; const idMPEG7_VisualCodingFormatCS = "VisualCodingFormatCS.xml"; -export const MPEG7_VisualCodingFormatCS = { file: join(pathMPEG7, idMPEG7_VisualCodingFormatCS), url: `${REPO_RAW}${pathMPEG7}/${idMPEG7_VisualCodingFormatCS}` }; +export const MPEG7_VisualCodingFormatCS = { file: join(pathMPEG7, idMPEG7_VisualCodingFormatCS), url: `${REPO_RAW}${subdirMPEG7}/${idMPEG7_VisualCodingFormatCS}` }; const idMPEG7_AudioPresentationCS = "AudioPresentationCS.xml"; -export const MPEG7_AudioPresentationCS = { file: join(pathMPEG7, idMPEG7_AudioPresentationCS), url: `${REPO_RAW}${pathMPEG7}/${idMPEG7_AudioPresentationCS}` }; +export const MPEG7_AudioPresentationCS = { file: join(pathMPEG7, idMPEG7_AudioPresentationCS), url: `${REPO_RAW}${subdirMPEG7}/${idMPEG7_AudioPresentationCS}` }; const idDVB_AudioConformanceCS = "AudioConformancePointsCS.xml"; export const DVB_AudioConformanceCS = { file: join(path2017CS, idDVB_AudioConformanceCS), url: `${DVB_METADATA}${url2017CS}/${idDVB_AudioConformanceCS}` }; @@ -105,19 +116,20 @@ export const DVB_VideoConformanceCS = { y2017: { file: join(path2017CS, idDVB_VideoConformanceCS), url: `${DVB_METADATA}${url2017CS}/${idDVB_VideoConformanceCS}` }, y2021: { file: join(path2021CS, idDVB_VideoConformanceCS), url: `${DVB_METADATA}${url2021CS}/${idDVB_VideoConformanceCS}` }, y2022: { file: join(path2022CS, idDVB_VideoConformanceCS), url: `${DVB_METADATA}${url2022CS}/${idDVB_VideoConformanceCS}` }, + y2024: { file: join(path2024CS, idDVB_VideoConformanceCS), url: `${DVB_METADATA}${url2024CS}/${idDVB_VideoConformanceCS}` }, }; const idISO3166 = "iso3166-countries.json"; -export const ISO3166 = { file: join(pathISO, idISO3166), url: `${REPO_RAW}${pathISO}/${idISO3166}` }; +export const ISO3166 = { file: join(pathISO, idISO3166), url: `${REPO_RAW}${subdirISO}/${idISO3166}` }; const idDVBI_RecordingInfoCS = "DVBRecordingInfoCS-2019.xml"; -export const DVBI_RecordingInfoCS = { file: join(pathDVBI, idDVBI_RecordingInfoCS), url: `${REPO_RAW}${pathDVBI}/${idDVBI_RecordingInfoCS}` }; +export const DVBI_RecordingInfoCS = { file: join(pathDVBI, idDVBI_RecordingInfoCS), url: `${REPO_RAW}${subdirDVBI}/${idDVBI_RecordingInfoCS}` }; const v1Credits = "CreditsItem@role-values.txt"; -export const DVBI_CreditsItemRoles = { file: join(pathDVBI, v1Credits), url: `${REPO_RAW}${pathDVBI}/${v1Credits}` }; +export const DVBI_CreditsItemRoles = { file: join(pathDVBI, v1Credits), url: `${REPO_RAW}${subdirDVBI}/${v1Credits}` }; const v2Credits = "CreditsItem@role-values-v2.txt"; -export const DVBIv2_CreditsItemRoles = { file: join(pathDVBI, v2Credits), url: `${REPO_RAW}${pathDVBI}/${v2Credits}` }; +export const DVBIv2_CreditsItemRoles = { file: join(pathDVBI, v2Credits), url: `${REPO_RAW}${subdirDVBI}/${v2Credits}` }; const idDVB_ParentalGuidanceCS = "ParentalGuidanceCS.xml"; export const DVBI_ParentalGuidanceCS = { file: join(path2007CS, idDVB_ParentalGuidanceCS), url: `${DVB_METADATA}${url2007CS}/${idDVB_ParentalGuidanceCS}` }; @@ -139,6 +151,7 @@ export const DVBI_ServiceListSchema = { r4: { file: join(__dirname, "dvbi_v4.0-with-hls-hbbtv.xsd") }, r5: { file: join(__dirname, "dvbi_v5.0-with-hls-hbbtv.xsd") }, r6: { file: join(__dirname, "dvbi_v6.0-with-hls-hbbtv.xsd") }, + r7: { file: join(__dirname, "dvbi_v7.0-with-hls-hbbtv.xsd") }, }; const languagesFilename = "language-subtag-registry"; diff --git a/docker-compose.yml b/docker-compose.yml index f4c4a3f..c9f6227 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,10 @@ version: '1.0' -volumes: - node_modules: services: dvb-i-tools: + # Comment build and uncomment image if you want to use prebuilt image rather than build from the source code build: . + #image: ghcr.io/ccma-enginyeria/dvb-i-tools/dvb-i-tools:latest environment: - no_proxy=localhost,127.0.0.1 - volumes: - - "${GIT_CLONE_PATH:-./}:/usr/src/app" - - node_modules:/usr/src/app/node_modules ports: - "3030:3030" \ No newline at end of file diff --git a/dvb-i-tools.service b/dvb-i-tools.service new file mode 100644 index 0000000..6c77331 --- /dev/null +++ b/dvb-i-tools.service @@ -0,0 +1,25 @@ +# +# run as a Service +# +# link this file to /usr/lib/systemd/system/dvb-i-tools.service +# $sudo ln -s dvb-i-tools.service /usr/lib/systemd/system/dvb-i-tools.service +# +# modify the ExecStart location to the install directory +# +# start it +# #sudo service dvb-i-tools start +# +# get the status with +# $journalctl -lf -u dvb-i-tools + +[Unit] +Description=DVB-I validator +After=network-online.target + +[Service] +Restart=on-failure +WorkingDirectory=/home/paul/dvb-i-tools/ +ExecStart=/usr/bin/node /home/paul/dvb-i-tools/all-in-one.js + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/dvb/cs/2024/AudioCodecCS.xml b/dvb/cs/2024/AudioCodecCS.xml new file mode 100644 index 0000000..541fb06 --- /dev/null +++ b/dvb/cs/2024/AudioCodecCS.xml @@ -0,0 +1,173 @@ + + + + + + MPEG-4 DVB Audio + + MPEG-4 Advanced Audio Profile + + MPEG-4 Advanced Audio Profile @ Level 1 + + + MPEG-4 Advanced Audio Profile @ Level 2 + + + MPEG-4 Advanced Audio Profile @ Level 4 + + + MPEG-4 Advanced Audio Profile @ Level 5 + + + + MPEG-4 High Efficiency Advanced Audio Profile + + MPEG-4 High Efficiency Advanced Audio Profile @ Level 2 + + + MPEG-4 High Efficiency Advanced Audio Profile @ Level 3 + + + MPEG-4 High Efficiency Advanced Audio Profile @ Level 4 + + + MPEG-4 High Efficiency Advanced Audio Profile @ Level 5 + + + + MPEG-4 High Efficiency Advanced Audio v2 Profile + + MPEG-4 High Efficiency Advanced Audio v2 Profile @ Level 2 + + + MPEG-4 High Efficiency Advanced Audio v2 Profile @ Level 3 + + + MPEG-4 High Efficiency Advanced Audio v2 Profile @ Level 4 + + + MPEG-4 High Efficiency Advanced Audio v2 Profile @ Level 5 + + + + + AMR DVB + Advanced Multi Rate + + AMR-WB+ - Extended AMR Wideband codec + ETSI TS 126 290 Extended AMR Wideband codec + + + + AC3 + + E-AC3 + Dolby Enhanced AC3 + + + + AC-4 + + AC-4 CIP + AC-4 Channel-based, Immersive and Personalized audio as specified in ETSI TS 103 190-2 + + AC-4 CIP Level 0 + + + AC-4 CIP Level 1 + + + AC-4 CIP Level 2 + + + AC-4 CIP Level 3 + + + + + DTS + + DTS-HD + + DTS-HD Core + DTS Coherent Acoustics Core substream multichannel audio as specified in ETSI TS 102 114 + + + DTS-HD LBR + DTS LBR Extension substream multichannel audio as specified in ETSI TS 102 114 + + + DTS-HD Core+Extension Substream + DTS Core substream plus Extension substream as specified in ETSI TS 102 114 + + + DTS-HD Lossless + DTS-HD Lossless substream only as specified in ETSI TS 102 114 + + + + DTS-UHD + + DTS-UHD Profile 2 + DTS-UHD Profile 2 Immersive audio as specified in ETSI TS 103 491 + + + DTS-UHD Profile 3 + DTS-UHD Profile 3 Personalized and Immersive audio as specified in ETSI TS 103 491 + + + + + MPEG-H 3D Audio + + MPEG-H 3D Audio Low Complexity Profile + + MPEG-H 3D Audio Low Complexity Profile @ Level 1 + MPEG-H 3D Audio ISO/IEC 23008-3 + + + MPEG-H 3D Audio Low Complexity Profile @ Level 2 + MPEG-H 3D Audio ISO/IEC 23008-3 + + + MPEG-H 3D Audio Low Complexity Profile @ Level 3 + MPEG-H 3D Audio ISO/IEC 23008-3 + + + + + AVS3 Part 3 Audio + + AVS3P3 Lossless + AVS3P3 Lossless bitstream as specified in T/AI 109.3 + + + AVS3P3 General Full Rate + AVS3P3 General Full Rate bitstream as specified in T/AI 109.3 + + AVS3P3 Channel-based + AVS3P3 channel based bitstream + + + AVS3P3 Object-based + AVS3P3 object based bitstream + + + AVS3P3 Channel and Object-based + AVS3P3 Combined channel and object bitstream + + + AVS3P3 HOA + AVS3P3 High Order Ambisonic bitstream + + + + diff --git a/dvb/cs/2024/AudioConformancePointsCS.xml b/dvb/cs/2024/AudioConformancePointsCS.xml new file mode 100644 index 0000000..8d5a218 --- /dev/null +++ b/dvb/cs/2024/AudioConformancePointsCS.xml @@ -0,0 +1,83 @@ + + + + Broadcast Conformance Points + + Broadcast Receiver Conformance Points + + urn:dvb:broadcast:ird:audio:MPEG-1_and_MPEG-2_backwards_compatible + MPEG-1 and MPEG-2 backwards compatible audio as defined in clause 6.1 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:audio:AC-3_and_enhanced_AC-3 + AC-3 and enhanced AC-3 audio as defined in clause 6.2 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:audio:DTS + DTS audio as defined in clause 6.3 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:audio:MPEG-4_AAC_family + MPEG-4 AAC, MPEG-4 HE AAC and MPEG-4 HE AAC v2 audio as defined in clause 6.4 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:audio:AC-4_channel_based + AC-4 channel based audio as defined in clause 6.6 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:audio:AC-4_channel_based_immersive_personalized + AC-4 for channel-based, immersive and personalized audio as defined in clause 6.7 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:audio:MPEG-H + MPEG-H audio as defined in clause 6.8 of ETSI TS 101 154 + + + + Broadcast Bitstream Conformance Points + + urn:dvb:broadcast:bitstream:audio:MPEG-1_and_MPEG-2_backwards_compatible + MPEG-1 and MPEG-2 backwards compatible audio as defined in clause 6.1 of ETSI TS 101 154 + + + urn:dvb:broadcast:bitstream:audio:AC-3_and_enhanced_AC-3 + AC-3 and enhanced AC-3 audio as defined in clause 6.2 of ETSI TS 101 154 + + + urn:dvb:broadcast:bitstream:audio:DTS + DTS audio as defined in clause 6.3 of ETSI TS 101 154 + + + urn:dvb:broadcast:bitstream:audio:MPEG-4_AAC_family + MPEG-4 AAC, MPEG-4 HE AAC and MPEG-4 HE AAC v2 audio as defined in clause 6.4 of ETSI TS 101 154 + + + urn:dvb:broadcast:bitstream:audio:AC-4_channel_based + AC-4 channel based audio as defined in clause 6.6 of ETSI TS 101 154 + + + urn:dvb:broadcast:bitstream:audio:AC-4_channel_based_immersive_personalized + AC-4 for channel-based, immersive and personalized audio as defined in clause 6.7 of ETSI TS 101 154 + + + urn:dvb:broadcast:bitstream:audio:MPEG-H + MPEG-H audio as defined in clause 6.8 of ETSI TS 101 154 + + + urn:dvb:broadcast:bitstream:audio:AVS3P3 + Channel, Object, Channel+Object or HOA coded audio according to AVS3 Part 3 Audio, T/AI 109.3 + + + + + DASH Conformance Points + + DASH Player Conformance Points + + + DASH Bitstream Conformance Points + + + diff --git a/dvb/cs/2024/VideoConformancePointsCS.xml b/dvb/cs/2024/VideoConformancePointsCS.xml new file mode 100644 index 0000000..396bf39 --- /dev/null +++ b/dvb/cs/2024/VideoConformancePointsCS.xml @@ -0,0 +1,197 @@ + + + + + Broadcast Conformance Points + + Broadcast Receiver Conformance Points + + urn:dvb:broadcast:ird:video:25_Hz_H.264_AVC_HDTV_IRD + 25 Hz H.264/AVC HDTV IRD as defined in clause 5.7.2 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:30_Hz_H.264_AVC_HDTV_IRD + 30 Hz H.264/AVC HDTV IRD as defined in clause 5.7.3 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:50_Hz_H.264_AVC_HDTV_IRD + 50 Hz H.264/AVC HDTV IRD as defined in clause 5.7.4 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:60_Hz_H.264_AVC_HDTV_IRD + 60 Hz H.264/AVC HDTV IRD as defined in clause 5.7.5 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:50_Hz_HEVC_HDTV_8-bit_IRD + 50 Hz HEVC HDTV 8-bit IRD as defined in table 18a in clause 5.14.1.0 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:60_Hz_HEVC_HDTV_8-bit_IRD + 60 Hz HEVC HDTV 8-bit IRD as defined in table 18a in clause 5.14.1.0 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:50_Hz_HEVC_HDTV_10-bit_IRD + 50 Hz HEVC HDTV 10-bit IRD as defined in table 18a in clause 5.14.1.0 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:60_Hz_HEVC_HDTV_10-bit_IRD + 60 Hz HEVC HDTV 10-bit IRD as defined in table 18a in clause 5.14.1.0 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:HEVC_UHDTV_IRD + HEVC UHDTV IRD as defined in table 18a in clause 5.14.1.0 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:HEVC_HDR_UHDTV_IRD_using_HLG10 + HEVC HDR UHDTV IRD using HLG10 as defined in table 18a in clause 5.14.1.0 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:HEVC_HDR_UHDTV_IRD_using_PQ10 + HEVC HDR UHDTV IRD using PQ10 as defined in table 18a in clause 5.14.1.0 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:HEVC_HDR_HFR_UHDTV_IRD_using_HLG10 + HEVC HDR HFR UHDTV IRD using HLG10 as defined in table 18a in clause 5.14.1.0 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:HEVC_HDR_HFR_UHDTV_IRD_using_PQ10 + HEVC HDR HFR UHDTV IRD using PQ10 as defined in table 18a in clause 5.14.1.0 of ETSI TS 101 154 + + + urn:dvb:broadcast:ird:video:HEVC_HDR_UHDTV2_IRD + HEVC HDR UHDTV2 IRD as defined in table 18a in clause 5.14.1.0 of ETSI TS 101 154 + + + + Broadcast Bitstream Conformance Points + + + + DASH Conformance Points + + DASH Player Conformance Points + + urn:dvb:dash:player:video:avc_hd_50_level40 + avc_hd_50_level40 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:avc_hd_60_level40 + avc_hd_60_level40 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:avc_hd_50 + avc_hd_50 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:avc_hd_60 + avc_hd_60 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:hevc_hd_50_8 + hevc_hd_50_8 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:hevc_hd_60_8 + hevc_hd_60_8 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:hevc_hd_50_10 + hevc_hd_50_10 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:hevc_hd_60_10 + hevc_hd_60_10 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:hevc_uhd + hevc_uhd DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:hevc_uhd_hlg10 + hevc_uhd_hlg10 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:hevc_uhd_pq10 + hevc_uhd_pq10 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:hevc_uhd_hfr_hlg10 + hevc_uhd_hfr_hlg10 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:hevc_uhd_hfr_pq10 + hevc_uhd_hfr_pq10 DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:hevc_uhd2_hdr + hevc_uhd2_hdr DASH player as defined in table L.1 in clause L.2.1 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:avs3_uhd1_hdr + AVS3 UHDTV-1 DASH player as defined in clause 5.16.3 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:avs3_uhd1_hdr_hfr + AVS3 UHDTV-1 HFRDASH player as defined in clause 5.16.4 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:avs3_uhd2_hdr + AVS3 UHDTV-2 DASH player as defined in clause 5.16.5 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:avs3_uhd2_hdr_hfr + AVS3 UHDTV-2 HFR DASH player as defined in clause 5.16.6 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:vvc_uhd1_hdr + VVC UHDTV-1 DASH player as defined in clause 5.15.2 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:vvc_uhd1_hdr_hfr + VVC UHDTV-1 HFRDASH player as defined in clause 5.15.3 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:vvc_uhd2_hdr + VVC UHDTV-2 DASH player as defined in clause 5.15.4 of ETSI TS 101 154 + + + urn:dvb:dash:player:video:vvc_uhd2_hdr_hfr + VVC UHDTV-2 HFR DASH player as defined in clause 5.15.5 of ETSI TS 101 154 + + + + DASH Bitstream Conformance Points + + urn:dvb:dash:bitstream:video:hdr_hlg10 + High dynamic range UHD bitstream using HLG10 as defined in clauses 5.2.6, 5.3.6 and 5.4.6 of ETSI TS 103 285 + + + urn:dvb:dash:bitstream:video:hdr_pq10 + High dynamic range UHD bitstream using PQ10 as defined in clauses 5.2.7, 5.3.7 and 5.4.7 of ETSI TS 103 285 + + + + DASH Bitstream HDR Dynamic Mapping Information Conformance Points + + urn:dvb:dash:hdr-dmi:st2094-10 + High dynamic range UHD bitstream using PQ10 as defined in clauses 5.2.7, 5.3.7 and 5.4.8 of ETSI TS 103 285, and HDR dynamic mapping information conforming to the encoding constraints in clause L.3.3.10.4.2, 5.15.1.10.3.4.3 or 5.16.2.5.4.3 of ETSI TS 101 154 + + + urn:dvb:dash:hdr-dmi:sl-hdr2 + High dynamic range UHD bitstream using PQ10 as defined in clauses 5.2.7, 5.3.7 and 5.4.7 of ETSI TS 103 285, and HDR dynamic mapping information conforming to the encoding constraints in clause L.3.3.10.4.3, 5.15.1.10.3.4.4 or 5.16.2.5.4.4 of ETSI TS 101 154 + + + urn:dvb:dash:hdr-dmi:st2094-40 + High dynamic range UHD bitstream using PQ10 as defined in clauses 5.2.7, 5.3.7 and 5.4.7 of ETSI TS 103 285, and HDR dynamic mapping information conforming to the encoding constraints in clause L.3.3.10.4.4, 5.15.1.10.3.4.5 or 5.16.2.5.4.5 of ETSI TS 101 154 + + + + + \ No newline at end of file diff --git a/dvbi_v7.0-with-hls-hbbtv.xsd b/dvbi_v7.0-with-hls-hbbtv.xsd new file mode 100644 index 0000000..7dee6a4 --- /dev/null +++ b/dvbi_v7.0-with-hls-hbbtv.xsd @@ -0,0 +1,855 @@ + + + + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The use of this element is deprecated in this version of the specification, in favour of the client application making + a delivery system determination based on the specified delivery parameters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Representation of the minimum age rating values specifed in DVB-SI + 0x01 = 4, 0xF = 18 + + + + + + + + + + + + The use of this element is deprecated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DVB-S2X only + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The use of this element is deprecated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Monday + + + + + Sunday + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The use of this element is deprecated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The use of this element is deprecated + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + union of DomainType and IPType + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + union of IPv4Type and IPv6Type + + + + + + Regular expressions in pattern values for type define compatible address structures for IPv4 syntax + + + + + + + + Regular expressions in pattern values for type define compatible address structures IPv6 syntax + + + + + + diff --git a/dvbi_v7.0.xsd b/dvbi_v7.0.xsd new file mode 100644 index 0000000..9e935f6 --- /dev/null +++ b/dvbi_v7.0.xsd @@ -0,0 +1,853 @@ + + + + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The use of this element is deprecated in this version of the specification, in favour of the client application making + a delivery system determination based on the specified delivery parameters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Representation of the minimum age rating values specifed in DVB-SI + 0x01 = 4, 0xF = 18 + + + + + + + + + + + + The use of this element is deprecated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DVB-S2X only + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The use of this element is deprecated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Monday + + + + + Sunday + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The use of this element is deprecated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The use of this element is deprecated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + union of DomainType and IPType + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + union of IPv4Type and IPv6Type + + + + + + Regular expressions in pattern values for type define compatible address structures for IPv4 syntax + + + + + + + + Regular expressions in pattern values for type define compatible address structures IPv6 syntax + + + + + + diff --git a/fetch_err_handler.js b/fetch_err_handler.js index dedec3f..c88345a 100644 --- a/fetch_err_handler.js +++ b/fetch_err_handler.js @@ -1,12 +1,12 @@ /** - * fetch_error_handler.js - * + * fetch_err_handler.js + * * Throw a nice error is there is a problem fetching the information * * @param {*} response * @returns */ -export function handleErrors(response) { +export default function handleErrors(response) { if (response && !response.ok) throw Error(`fetch() returned (${response.status}) "${response.statusText}"`); return response; } diff --git a/hbbtv-ext-6.0.xsd b/hbbtv-ext-6.0.xsd index efbf530..ee796b7 100644 --- a/hbbtv-ext-6.0.xsd +++ b/hbbtv-ext-6.0.xsd @@ -1,6 +1,6 @@ - - + + diff --git a/hbbtv-ext-7.0.xsd b/hbbtv-ext-7.0.xsd new file mode 100644 index 0000000..ee796b7 --- /dev/null +++ b/hbbtv-ext-7.0.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/hls-url-7.0.xsd b/hls-url-7.0.xsd new file mode 100644 index 0000000..b40b45c --- /dev/null +++ b/hls-url-7.0.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/logger.js b/logger.js new file mode 100644 index 0000000..f413861 --- /dev/null +++ b/logger.js @@ -0,0 +1,44 @@ +/** + * logger.js + * + * log stuff from the validator + */ +import chalk from "chalk"; + +import { existsSync, writeFile } from "fs"; +import { join, sep } from "path"; +import { MODE_URL, MODE_SL } from "./ui.js"; + +export function createPrefix(req) { + const logDir = join(".", "arch"); + + if (!existsSync(logDir)) return null; + + const getDate = (d) => { + const fillZero = (t) => (t < 10 ? `0${t}` : t); + return `${d.getFullYear()}-${fillZero(d.getMonth() + 1)}-${fillZero(d.getDate())} ${fillZero(d.getHours())}.${fillZero(d.getMinutes())}.${fillZero(d.getSeconds())}`; + }; + + const fname = req.body.doclocation == MODE_URL ? req.body.XMLurl.substr(req.body.XMLurl.lastIndexOf("/") + 1) : req?.files?.XMLfile?.name; + if (!fname) return null; + + return `${logDir}${sep}${getDate(new Date())} (${req.body.testtype == MODE_SL ? "SL" : req.body.requestType}) ${fname.replace(/[/\\?%*:|"<>]/g, "-")}`; +} + +export default function writeOut(errs, filebase, markup, req = null) { + if (!filebase || errs.markupXML?.length == 0) return; + + let outputLines = []; + if (markup && req?.body?.XMLurl) outputLines.push(``); + errs.markupXML.forEach((line) => { + outputLines.push(line.value); + if (markup && line.validationErrors) + line.validationErrors.forEach((error) => { + outputLines.push(``); + }); + }); + const filename = markup ? `${filebase}.mkup.txt` : `${filebase}.raw.txt`; + writeFile(filename, outputLines.join("\n"), (err) => { + if (err) console.log(chalk.red(err)); + }); +} diff --git a/package.json b/package.json index 739ba26..efba231 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dvb-i-tools", - "version": "1.1.6", - "description": "DVB-I V&V Tools", + "version": "1.7.1", + "description": "DVB-I V&V Tools - up to A177r7", "main": "all-in-one.js", "type": "module", "private": true, @@ -12,17 +12,18 @@ "dependencies": { "@datastructures-js/binary-search-tree": "^5.3.2", "chalk": "^5.3.0", - "command-line-args": "^6.0.0", + "command-line-args": "^6.0.1", "command-line-usage": "^7.0.3", "cors": "^2.8.5", - "express": "^4.19.2", + "express": "^4.21.1", "express-fileupload": "^1.5.1", - "express-session": "^1.18.0", + "express-session": "^1.18.1", "libxmljs2": "^0.35.0", "morgan": "^1.10.0", "serve-favicon": "^2.5.0", "sync-fetch": "^0.5.2", - "xml-formatter": "^3.6.3" + "xml-formatter": "^3.6.3", + "xmllint": "^0.1.1" }, "engines": { "node": ">= 20.10.0", @@ -36,6 +37,6 @@ "url": "https://github.com/paulhiggs/dvb-i-tools" }, "devDependencies": { - "eslint": "^9.9.0" + "eslint": "^9.11.1" } } diff --git a/phlib b/phlib index 3a9de35..e7ea54d 160000 --- a/phlib +++ b/phlib @@ -1 +1 @@ -Subproject commit 3a9de3502047e5aab2415644aed1517fa09dd113 +Subproject commit e7ea54d8f82fec95899d954ed7fb9f933ae8f342 diff --git a/registries/slepr-main.xml b/registries/slepr-main.xml index d71eb80..a82254d 100644 --- a/registries/slepr-main.xml +++ b/registries/slepr-main.xml @@ -1,5 +1,8 @@ - + http://dvb.org/img/f/dvb-logo.png @@ -36,16 +39,17 @@ - Italian Trusted Services - - http://dvbi.italian-authority.it/trusted-services.xml - - - - - - it - ITA,SMR + Italian Trusted Services + + http://dvbi.italian-authority.it/trusted-services.xml + + + + + + it + ITA,SMR + it1 @@ -64,32 +68,34 @@ - TV services from the world in English - Fernsehen aus ter Welt in Englisch - Télévision du monde en anglais - TV del mondo in inglese - - http://dvbi.TVfromTheWorld.com/engTVservices.xml - - - - - en + TV services from the world in English + Fernsehen aus ter Welt in Englisch + Télévision du monde en anglais + TV del mondo in inglese + + http://dvbi.TVfromTheWorld.com/engTVservices.xml + + + + + en + world1 - TV aus Deutschland - TV from Germany - - http://dvbi.TVfromTheWorld.com/TVservices_Germany.xml - - - - - - de - en - DEU + TV aus Deutschland + TV from Germany + + http://dvbi.TVfromTheWorld.com/TVservices_Germany.xml + + + + + + de + en + DEU + germany1 @@ -107,26 +113,27 @@ - Documentaries - - + Documentaries + + http://www.british-service-list-provider.co.uk/documentaries.xml - - - - + + + + http://alt.british-service-list-provider.co.uk/documentaries.xml - - - - - - en - GBR - FRA - ITA - DEU - AUT + + + + + + en + GBR + FRA + ITA + DEU + AUT + uk1 @@ -144,27 +151,32 @@ - Germany FTA - - + Germany FTA + + http://www.satip.info/sites/satip/files/files/ASTRA_19_2_E.xml - - - - - - - DEU - - - + + + + + + 12 + + + DEU + + + https://www.satip.info/wp-content/themes/afina-child/images/satip-spacer.jpg - - - data:image/jpeg;base64,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 - - + + + +data:image/jpeg;base64,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 + + + + img1 @@ -182,14 +194,72 @@ - DVB-I Reference Client service list - - https://raw.githubusercontent.com/DVBproject/DVB-I-Reference-Client/master/backend/servicelists/example.xml - - - - - + DVB-I Reference Client service list + + https://raw.githubusercontent.com/DVBproject/DVB-I-Reference-Client/master/backend/servicelists/example.xml + + + + + + ex1 + + + + + Paul + + Paul + + Reading + + + + +44 00000000 + paul@dvb.org + + + + DVB-I Reference Client service list (inline images) + + https://raw.githubusercontent.com/DVBproject/DVB-I-Reference-Client/master/backend/servicelists/example.xml + + + + + + + + +data:image/jpeg;base64,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 + + + + image-inline111 + + + DVB-I Reference Client service list (linked images) + + https://raw.githubusercontent.com/DVBproject/DVB-I-Reference-Client/master/backend/servicelists/example.xml + + + + + + + + + https://github.com/paulhiggs/phlib/blob/e7ea54d8f82fec95899d954ed7fb9f933ae8f342/ph-icon.jpg + + + + + https://github.com/paulhiggs/phlib/blob/e7ea54d8f82fec95899d954ed7fb9f933ae8f342/ph-icon.png + + + + image-ref111 + diff --git a/role.js b/role.js index d5512a8..f1211f2 100644 --- a/role.js +++ b/role.js @@ -1,12 +1,12 @@ /** * role.js - * + * * Manages Classification Scheme checking based in a flat list of roles */ import chalk from "chalk"; import { readFile } from "fs"; -import { handleErrors } from "./fetch_err_handler.js"; +import handleErrors from "./fetch_err_handler.js"; import { isHTTPURL } from "./pattern_checks.js"; import ClassificationScheme from "./classification_scheme.js"; diff --git a/schema_checks.js b/schema_checks.js index 9da3723..37ed496 100644 --- a/schema_checks.js +++ b/schema_checks.js @@ -99,7 +99,12 @@ export function checkTopElementsAndCardinality(parentElement, childElements, def count = namedChildren.length; if (count == 0 && min != 0) { - errs.addError({ code: `${errCode}-1`, line: parentElement.line(), message: `Mandatory element ${elem.name.elementize()} not specified in ${thisElem}` }); + errs.addError({ + code: `${errCode}-1`, + line: parentElement.line(), + message: `Mandatory element ${elem.name.elementize()} not specified in ${thisElem}`, + key: "missing element", + }); rv = false; } else { if (count < min || count > max) { @@ -108,6 +113,7 @@ export function checkTopElementsAndCardinality(parentElement, childElements, def code: `${errCode}-2`, line: child.line(), message: `Cardinality of ${elem.name.elementize()} in ${thisElem} is not in the range ${min}..${max == Infinity ? "unbounded" : max}`, + key: "wrong element count", }) ); rv = false; @@ -127,9 +133,15 @@ export function checkTopElementsAndCardinality(parentElement, childElements, def let childName = child.name(); if (!findElementIn(childElements, childName)) { if (isIn(excludedChildren, childName)) - errs.addError({ type: INFORMATION, code: `${errCode}-10`, message: `Element ${childName.elementize()} in ${thisElem} is not included in DVB-I`, line: child.line() }); + errs.addError({ + type: INFORMATION, + code: `${errCode}-10`, + message: `Element ${childName.elementize()} in ${thisElem} is not included in DVB-I`, + line: child.line(), + key: "profiled out", + }); else if (!allowOtherElements) { - errs.addError({ code: `${errCode}-11`, line: child.line(), message: `Element ${childName.elementize()} is not permitted in ${thisElem}` }); + errs.addError({ code: `${errCode}-11`, line: child.line(), message: `Element ${childName.elementize()} is not permitted in ${thisElem}`, key: "element not allowed" }); rv = false; } } diff --git a/sl_check.js b/sl_check.js index 987c296..1d17de7 100644 --- a/sl_check.js +++ b/sl_check.js @@ -1,32 +1,26 @@ /** * sl_check.js - * + * * Check a service list */ -import { readFileSync } from "fs"; -import process from "process"; - - - import chalk from "chalk"; -import { parseXmlString } from "libxmljs2"; import { elementize, quote } from "./phlib/phlib.js"; import { tva, tvaEA } from "./TVA_definitions.js"; import { sats } from "./DVB_definitions.js"; import { dvbi, dvbiEC, dvbEA, XMLdocumentType } from "./DVB-I_definitions.js"; -import { OLD, DRAFT, ETSI, CURRENT } from "./globals.js"; + import ErrorList, { WARNING, APPLICATION } from "./error_list.js"; import { isTAGURI } from "./URI_checks.js"; import { xPath, xPathM, isIn, unEntity, getElementByTagName, DuplicatedValue } from "./utils.js"; import { isPostcode, isASCII, isHTTPURL, isHTTPPathURL, isDomainName, isRTSPURL } from "./pattern_checks.js"; -import { DVBI_ServiceListSchema, __dirname_linux } from "./data_locations.js"; +import { __dirname_linux } from "./data_locations.js"; import { checkValidLogos } from "./related_material_checks.js"; import { sl_InvalidHrefValue, InvalidURL, DeprecatedElement, keys } from "./common_errors.js"; import { mlLanguage, checkLanguage, checkXMLLangs, GetNodeLanguage } from "./multilingual_element.js"; import { checkAttributes, checkTopElementsAndCardinality, hasChild, SchemaCheck, SchemaVersionCheck, SchemaLoad } from "./schema_checks.js"; -import { writeOut } from "./validator.js"; +import writeOut from "./logger.js"; import { LoadGenres, LoadVideoCodecCS, @@ -46,10 +40,42 @@ import { LoadLanguages, LoadCountries, } from "./classification_scheme_loaders.js"; -import { CheckAccessibilityAttributes } from "./accessibility_attributes_checks.js"; +import CheckAccessibilityAttributes from "./accessibility_attributes_checks.js"; import { DASH_IF_Content_Protection_List, ContentProtectionIDs, CA_SYSTEM_ID_REGISTRY, CASystemIDs } from "./identifiers.js"; -const ANY_NAMESPACE = "$%$!!"; +import { + GetSchema, + SchemaVersion, + SchemaSpecVersion, + ANY_NAMESPACE, + SCHEMA_r0, + SCHEMA_r1, + SCHEMA_r2, + SCHEMA_r3, + SCHEMA_r4, + SCHEMA_r5, + SCHEMA_r6, + SCHEMA_r7, + SCHEMA_unknown, +} from "./sl_data_versions.js"; +import { + validServiceControlApplication, + validAgreementApplication, + validServiceInstanceControlApplication, + validServiceUnavailableApplication, + validDASHcontentType, +} from "./sl_data_versions.js"; +import { + validOutScheduleHours, + validContentFinishedBanner, + validServiceListLogo, + validServiceAgreementApp, + validServiceLogo, + validServiceBanner, + validContentGuideSourceLogo, +} from "./sl_data_versions.js"; +import { CMCD_keys, checkCMCDkeys } from "./CMCD.js"; + const LCN_TABLE_NO_TARGETREGION = "unspecifiedRegion", LCN_TABLE_NO_SUBSCRIPTION = "unspecifiedPackage"; @@ -58,143 +84,6 @@ const SERVICE_RM = "service"; const SERVICE_INSTANCE_RM = "service instance"; const CONTENT_GUIDE_RM = "content guide"; -const SCHEMA_r0 = 0, - SCHEMA_r1 = 1, - SCHEMA_r2 = 2, - SCHEMA_r3 = 3, - SCHEMA_r4 = 4, - SCHEMA_r5 = 5, - SCHEMA_r6 = 6, - SCHEMA_unknown = -1; - -let SchemaVersions = [ - // schema property is loaded from specified filename - { - namespace: dvbi.A177r6_Namespace, - version: SCHEMA_r6, - filename: DVBI_ServiceListSchema.r6.file, - schema: null, - status: CURRENT, - specVersion: "A177r6", - }, - { - namespace: dvbi.A177r5_Namespace, - version: SCHEMA_r5, - filename: DVBI_ServiceListSchema.r5.file, - schema: null, - status: OLD, - specVersion: "A177r5", - }, - { - namespace: dvbi.A177r4_Namespace, - version: SCHEMA_r4, - filename: DVBI_ServiceListSchema.r4.file, - schema: null, - status: OLD, - specVersion: "A177r4", - }, - { - namespace: dvbi.A177r3_Namespace, - version: SCHEMA_r3, - filename: DVBI_ServiceListSchema.r3.file, - schema: null, - status: OLD, - specVersion: "A177r3", - }, - { - namespace: dvbi.A177r2_Namespace, - version: SCHEMA_r2, - filename: DVBI_ServiceListSchema.r2.file, - schema: null, - status: OLD, - specVersion: "A177r2", - }, - { - namespace: dvbi.A177r1_Namespace, - version: SCHEMA_r1, - filename: DVBI_ServiceListSchema.r1.file, - schema: null, - status: ETSI, - specVersion: "A177r1", - }, - { - namespace: dvbi.A177_Namespace, - version: SCHEMA_r0, - filename: DVBI_ServiceListSchema.r0.file, - schema: null, - status: OLD, - specVersion: "A177", - }, -]; - -const OutOfScheduledHoursBanners = [ - { ver: SCHEMA_r6, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v3 }, - { ver: SCHEMA_r5, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v3 }, - { ver: SCHEMA_r4, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v3 }, - { ver: SCHEMA_r3, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v3 }, - { ver: SCHEMA_r2, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v2 }, - { ver: SCHEMA_r1, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v2 }, - { ver: SCHEMA_r0, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v1 }, -]; -const ContentFinishedBanners = [ - { ver: SCHEMA_r6, val: dvbi.BANNER_CONTENT_FINISHED_v3 }, - { ver: SCHEMA_r5, val: dvbi.BANNER_CONTENT_FINISHED_v3 }, - { ver: SCHEMA_r4, val: dvbi.BANNER_CONTENT_FINISHED_v3 }, - { ver: SCHEMA_r3, val: dvbi.BANNER_CONTENT_FINISHED_v3 }, - { ver: SCHEMA_r2, val: dvbi.BANNER_CONTENT_FINISHED_v2 }, - { ver: SCHEMA_r1, val: dvbi.BANNER_CONTENT_FINISHED_v2 }, -]; -const ServiceListLogos = [ - { ver: SCHEMA_r6, val: dvbi.LOGO_SERVICE_LIST_v3 }, - { ver: SCHEMA_r5, val: dvbi.LOGO_SERVICE_LIST_v3 }, - { ver: SCHEMA_r4, val: dvbi.LOGO_SERVICE_LIST_v3 }, - { ver: SCHEMA_r3, val: dvbi.LOGO_SERVICE_LIST_v3 }, - { ver: SCHEMA_r2, val: dvbi.LOGO_SERVICE_LIST_v2 }, - { ver: SCHEMA_r1, val: dvbi.LOGO_SERVICE_LIST_v2 }, - { ver: SCHEMA_r0, val: dvbi.LOGO_SERVICE_LIST_v1 }, -]; -const ServiceLogos = [ - { ver: SCHEMA_r6, val: dvbi.LOGO_SERVICE_v3 }, - { ver: SCHEMA_r5, val: dvbi.LOGO_SERVICE_v3 }, - { ver: SCHEMA_r4, val: dvbi.LOGO_SERVICE_v3 }, - { ver: SCHEMA_r3, val: dvbi.LOGO_SERVICE_v3 }, - { ver: SCHEMA_r2, val: dvbi.LOGO_SERVICE_v2 }, - { ver: SCHEMA_r1, val: dvbi.LOGO_SERVICE_v2 }, - { ver: SCHEMA_r0, val: dvbi.LOGO_SERVICE_v1 }, -]; -const ServiceBanners = [ - { ver: SCHEMA_r6, val: dvbi.SERVICE_BANNER_v4 }, - { ver: SCHEMA_r5, val: dvbi.SERVICE_BANNER_v4 }, - { ver: SCHEMA_r4, val: dvbi.SERVICE_BANNER_v4 }, - { ver: SCHEMA_r3, val: dvbi.SERVICE_BANNER_v4 }, - { ver: SCHEMA_r2, val: dvbi.SERVICE_BANNER_v4 }, -]; -const ContentGuideSourceLogos = [ - { ver: SCHEMA_r6, val: dvbi.LOGO_CG_PROVIDER_v3 }, - { ver: SCHEMA_r5, val: dvbi.LOGO_CG_PROVIDER_v3 }, - { ver: SCHEMA_r4, val: dvbi.LOGO_CG_PROVIDER_v3 }, - { ver: SCHEMA_r3, val: dvbi.LOGO_CG_PROVIDER_v3 }, - { ver: SCHEMA_r2, val: dvbi.LOGO_CG_PROVIDER_v2 }, - { ver: SCHEMA_r1, val: dvbi.LOGO_CG_PROVIDER_v2 }, - { ver: SCHEMA_r0, val: dvbi.LOGO_CG_PROVIDER_v1 }, -]; - -/** - * determine the schema version (and hence the specificaion version) in use - * - * @param {String} namespace The namespace used in defining the schema - * @returns {integer} Representation of the schema version or error code if unknown - */ -let SchemaVersion = (namespace) => { - const x = SchemaVersions.find((ver) => ver.namespace == namespace); - return x ? x.version : SCHEMA_unknown; -}; - -let SchemaSpecVersion = (namespace) => { - const x = SchemaVersions.find((ver) => ver.namespace == namespace); - return x ? x.specVersion : "r?"; -}; - const EXTENSION_LOCATION_SERVICE_LIST_REGISTRY = 101, EXTENSION_LOCATION_SERVICE_ELEMENT = 201, EXTENSION_LOCATION_DASH_INSTANCE = 202, @@ -219,44 +108,6 @@ let validServiceListIdentifier = (identifier) => isTAGURI(identifier); */ let uniqueServiceIdentifier = (identifier, identifiers) => !isIn(identifiers, identifier); -/** - * determines if the identifer provided refers to a valid application being used with the service - * - * @param {String} hrefType The type of the service application - * @param {integer} schemaVersion The schema version of the XML document - * @returns {boolean} true if this is a valid application being used with the service else false - */ -let validServiceControlApplication = (hrefType, schemaVersion) => { - let appTypes = [dvbi.APP_IN_PARALLEL, dvbi.APP_IN_CONTROL]; - if (schemaVersion >= SCHEMA_r6) appTypes.push(dvbi.APP_SERVICE_PROVIDER); - return appTypes.includes(hrefType); -}; - -/** - * determines if the identifer provided refers to a valid application being used with the service instance - * - * @param {String} hrefType The type of the service application - * @returns {boolean} true if this is a valid application being used with the service else false - */ -let validServiceInstanceControlApplication = (hrefType) => [dvbi.APP_IN_PARALLEL, dvbi.APP_IN_CONTROL].includes(hrefType); - -/** - * determines if the identifer provided refers to a valid application to be launched when a service is unavailable - * - * @param {String} hrefType The type of the service application - * @returns {boolean} true if this is a valid application to be launched when a service is unavailable else false - */ -let validServiceUnavailableApplication = (hrefType) => hrefType == dvbi.APP_OUTSIDE_AVAILABILITY; - -/** - * determines if the identifer provided refers to a valid DASH media type (single MPD or MPD playlist) - * per A177 clause 5.2.7.2 - * - * @param {String} contentType The contentType for the file - * @returns {boolean} true if this is a valid MPD or playlist identifier - */ -let validDASHcontentType = (contentType) => [dvbi.CONTENT_TYPE_DASH_MPD, dvbi.CONTENT_TYPE_DVB_PLAYLIST].includes(contentType); - /** * Add an error message an incorrect country code is specified in transmission parameters * @@ -384,7 +235,7 @@ export default class ServiceListCheck { #allowedVideoConformancePoints; #RecordingInfoCSvalues; - constructor(useURLs, opts, async=true) { + constructor(useURLs, opts, async = true) { this.#numRequests = 0; this.#knownLanguages = opts?.languages ? opts.languages : LoadLanguages(useURLs, async); @@ -408,18 +259,8 @@ export default class ServiceListCheck { this.#allowedAudioConformancePoints = LoadAudioConformanceCS(useURLs, async); this.#allowedVideoConformancePoints = LoadVideoConformanceCS(useURLs, async); this.#RecordingInfoCSvalues = LoadRecordingInfoCS(useURLs, async); - - // TODO - change this to support sync/asyna and file/url reading - console.log(chalk.yellow.underline("loading service list schemas...")); - SchemaVersions.forEach((version) => { - process.stdout.write(chalk.yellow(`..loading ${version.version} ${version.namespace} from ${version.filename} `)); - let schema = readFileSync(version.filename).toString().replace(`schemaLocation="./`, `schemaLocation="${__dirname_linux}/`); - version.schema = parseXmlString(schema); - console.log(version.schema ? chalk.green("OK") : chalk.red.bold("FAIL")); - }); } - stats() { let res = {}; res.numRequests = this.#numRequests; @@ -583,102 +424,6 @@ export default class ServiceListCheck { this.#addRegion(props, RegionChild, depth + 1, knownRegionIDs, countriesSpecified, errs); } - /** - * looks for the {index, value} pair within the array of permitted values - * - * @param {array} permittedValues array of allowed value pairs {ver: , val:} - * @param {any} version value to match with ver: in the allowed values or ANY_NAMESPACE - * @param {any} value value to match with val: in the allowed values - * @returns {boolean} true if {index, value} pair exists in the list of allowed values when namespace is specific or if any val: equals value with namespace is ANY_NAMESPACE, else false - */ - /*private*/ #match(permittedValues, version, value) { - if (value && permittedValues) { - if (version == ANY_NAMESPACE) return permittedValues.find((elem) => elem.value == value) != undefined; - else { - let i = permittedValues.find((elem) => elem.ver == version); - return i && i.val == value; - } - } - return false; - } - - /** - * determines if the identifer provided refers to a valid banner for out-of-servce-hours presentation - * - * @param {XMLnode} HowRelated The banner identifier - * @param {String} namespace The namespace being used in the XML document - * @returns {boolean} true if this is a valid banner for out-of-servce-hours presentation else false - */ - /*private*/ #validOutScheduleHours(HowRelated, namespace) { - // return true if val is a valid CS value for Out of Service Banners (A177 5.2.5.3) - return this.#match(OutOfScheduledHoursBanners, SchemaVersion(namespace), HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : null); - } - - /** - * determines if the identifer provided refers to a valid banner for content-finished presentation - * - * @since DVB A177r1 - * @param {XMLnode} HowRelated The banner identifier - * @param {String} namespace The namespace being used in the XML document - * @returns {boolean} true if this is a valid banner for content-finished presentation else false - */ - /*private*/ #validContentFinishedBanner(HowRelated, namespace) { - // return true if val is a valid CS value for Content Finished Banner (A177 5.2.7.3) - return this.#match( - ContentFinishedBanners, - namespace == ANY_NAMESPACE ? namespace : SchemaVersion(namespace), - HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : null - ); - } - - /** - * determines if the identifer provided refers to a valid service list logo - * - * @param {XMLnode} HowRelated The logo identifier - * @param {String} namespace The namespace being used in the XML document - * @returns {boolean} true if this is a valid logo for a service list else false - */ - /*private*/ #validServiceListLogo(HowRelated, namespace) { - // return true if HowRelated@href is a valid CS value Service List Logo (A177 5.2.6.1) - return this.#match(ServiceListLogos, SchemaVersion(namespace), HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : null); - } - - /** - * determines if the identifer provided refers to a valid service logo - * - * @param {XMLnode} HowRelated The logo identifier - * @param {String} namespace The namespace being used in the XML document - * @returns {boolean} true if this is a valid logo for a service else false - */ - /*private*/ #validServiceLogo(HowRelated, namespace) { - // return true if val is a valid CS value Service Logo (A177 5.2.6.2) - return this.#match(ServiceLogos, SchemaVersion(namespace), HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : null); - } - - /** - * determines if the identifer provided refers to a valid service banner - * - * @param {XMLnode} HowRelated The logo identifier - * @param {String} namespace The namespace being used in the XML document - * @returns {boolean} true if this is a valid banner for a service else false - */ - /*private*/ #validServiceBanner(HowRelated, namespace) { - // return true if val is a valid CS value Service Banner (A177 5.2.6.x) - return this.#match(ServiceBanners, SchemaVersion(namespace), HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : null); - } - - /** - * determines if the identifer provided refers to a valid content guide source logo - * - * @param {XMLnode} HowRelated The logo identifier - * @param {String} namespace The namespace being used in the XML document - * @returns {boolean} true if this is a valid logo for a content guide source else false - */ - /*private*/ #validContentGuideSourceLogo(HowRelated, namespace) { - // return true if val is a valid CS value Service Logo (A177 5.2.6.3) - return this.#match(ContentGuideSourceLogos, SchemaVersion(namespace), HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : null); - } - /** * verifies if the specified application is valid according to specification * @@ -826,25 +571,28 @@ export default class ServiceListCheck { if (HowRelated.attr(dvbi.a_href)) { switch (LocationType) { case SERVICE_LIST_RM: - if (this.#validServiceListLogo(HowRelated, props.namespace)) { + if (validServiceListLogo(HowRelated, props.namespace)) { rc = HowRelated.attr(dvbi.a_href).value(); checkValidLogos(RelatedMaterial, errs, `${errCode}-10`, Location, this.#knownLanguages); + } else if (validServiceAgreementApp(HowRelated, props.namespace)) { + rc = HowRelated.attr(dvbi.a_href).value(); + MediaLocator.forEach((locator) => this.#checkSignalledApplication(locator, errs, Location, rc)); } else errs.addError(sl_InvalidHrefValue(HowRelated.attr(dvbi.a_href).value(), HowRelated, tva.e_RelatedMaterial.elementize(), Location, `${errCode}-11`)); break; + case SERVICE_RM: - if (this.#validContentFinishedBanner(HowRelated, ANY_NAMESPACE) && SchemaVersion(props.namespace) == SCHEMA_r0) + if (validContentFinishedBanner(HowRelated, ANY_NAMESPACE) && SchemaVersion(props.namespace) == SCHEMA_r0) errs.addError({ code: `${errCode}-21`, message: `${HowRelated.attr(dvbi.href).value().quote()} not permitted for ${props.namespace.quote()} in ${Location}`, key: "invalid CS value", fragment: HowRelated, }); - if ( - this.#validOutScheduleHours(HowRelated, props.namespace) || - this.#validContentFinishedBanner(HowRelated, props.namespace) || - this.#validServiceLogo(HowRelated, props.namespace) || - this.#validServiceBanner(HowRelated, props.namespace) + validOutScheduleHours(HowRelated, props.namespace) || + validContentFinishedBanner(HowRelated, props.namespace) || + validServiceLogo(HowRelated, props.namespace) || + validServiceBanner(HowRelated, props.namespace) ) { rc = HowRelated.attr(dvbi.a_href).value(); checkValidLogos(RelatedMaterial, errs, `${errCode}-22`, Location, this.#knownLanguages); @@ -853,8 +601,9 @@ export default class ServiceListCheck { MediaLocator.forEach((locator) => this.#checkSignalledApplication(locator, errs, Location, rc)); } else errs.addError(sl_InvalidHrefValue(HowRelated.attr(dvbi.a_href).value(), HowRelated, tva.e_RelatedMaterial.elementize(), Location, `${errCode}-24`)); break; + case SERVICE_INSTANCE_RM: - if (this.#validContentFinishedBanner(HowRelated, ANY_NAMESPACE) && SchemaVersion(props.namespace) == SCHEMA_r0) + if (validContentFinishedBanner(HowRelated, ANY_NAMESPACE) && SchemaVersion(props.namespace) == SCHEMA_r0) errs.addError({ code: `${errCode}-31`, message: `${HowRelated.attr(dvbi.href).value().quote()} not permitted for ${props.namespace.quote()} in ${Location}`, @@ -862,10 +611,10 @@ export default class ServiceListCheck { fragment: HowRelated, }); - if (this.#validContentFinishedBanner(HowRelated, props.namespace) || this.#validServiceLogo(HowRelated, props.namespace)) { + if (validContentFinishedBanner(HowRelated, props.namespace) || validServiceLogo(HowRelated, props.namespace)) { rc = HowRelated.attr(dvbi.a_href).value(); checkValidLogos(RelatedMaterial, errs, `${errCode}-32`, Location, this.#knownLanguages); - } else if (this.#validOutScheduleHours(HowRelated, ANY_NAMESPACE) && SchemaVersion(props.namespace) >= SCHEMA_r6) { + } else if (validOutScheduleHours(HowRelated, ANY_NAMESPACE) && SchemaVersion(props.namespace) >= SCHEMA_r6) { errs.addError({ code: `${errCode}-35`, message: "Out of Service Banner is not permitted in a Service Instance from A177r6", @@ -877,7 +626,7 @@ export default class ServiceListCheck { clause: "A177 table 16", description: `Out of Service banner is not permitted in the ${tva.e_RelatedMaterial.elementize()} element of a ${dvbi.e_ServiceInstance.elementize()}`, }); - } else if (this.#validServiceBanner(HowRelated, props.namespace)) { + } else if (validServiceBanner(HowRelated, props.namespace)) { errs.addError({ code: `${errCode}-33`, message: "Service Banner is not permitted in a Service Instance", @@ -894,8 +643,9 @@ export default class ServiceListCheck { MediaLocator.forEach((locator) => this.#checkSignalledApplication(locator, errs, Location, rc)); } else errs.addError(sl_InvalidHrefValue(HowRelated.attr(dvbi.a_href).value(), HowRelated, tva.e_RelatedMaterial.elementize(), Location, `${errCode}-34`)); break; + case CONTENT_GUIDE_RM: - if (this.#validContentGuideSourceLogo(HowRelated, props.namespace)) { + if (validContentGuideSourceLogo(HowRelated, props.namespace)) { rc = HowRelated.attr(dvbi.a_href).value(); checkValidLogos(RelatedMaterial, errs, `${errCode}-41`, Location, this.#knownLanguages); } else errs.addError(sl_InvalidHrefValue(HowRelated.attr(dvbi.a_href).value(), HowRelated, tva.e_RelatedMaterial.elementize(), Location, `${errCode}-42`)); @@ -1295,6 +1045,15 @@ export default class ServiceListCheck { }); } + // + if (ServiceInstance.attr(dvbi.a_id) && ServiceInstance.attr(dvbi.a_id).value().length == 0) + errs.addError({ + code: "SI012", + message: `${dvbi.a_id.attribute()} should not be empty is specified`, + line: ServiceInstance.line(), + key: "empty ID", + }); + // checkXMLLangs(dvbi.e_DisplayName, `service instance in service=${thisServiceId.quote()}`, ServiceInstance, errs, "SI010", this.#knownLanguages); @@ -1347,9 +1106,7 @@ export default class ServiceListCheck { if (nestedCAsystemid) { CASystemID_value = nestedCAsystemid.text(); } - } - else - CASystemID_value = CASystemID.text(); + } else CASystemID_value = CASystemID.text(); if (CASystemID_value) { let CASid_value = parseInt(CASystemID_value, 10); if (isNaN(CASid_value)) { @@ -1385,8 +1142,7 @@ export default class ServiceListCheck { if (nestedDRMsystemid) { DRMSystemID_value = nestedDRMsystemid.text().toLowerCase(); } - } - else DRMSystemID_value = DRMSystemID.text().toLowerCase(); + } else DRMSystemID_value = DRMSystemID.text().toLowerCase(); if (DRMSystemID_value && ContentProtectionIDs.find((el) => el.id == DRMSystemID_value || el.id.substring(el.id.lastIndexOf(":") + 1) == DRMSystemID_value) == undefined) { errs.addError({ code: "SI033", @@ -1495,18 +1251,43 @@ export default class ServiceListCheck { } }); + // HDR DMI terms are in the 2.3 series + let isHDRDMISystem = (CSterm) => CSterm.substring(CSterm.lastIndexOf(":") + 1).startsWith("2.3."); + // Check @href of ContentAttributes/VideoConformancePoints cp = 0; - while ((conf = ContentAttributes.get(xPath(props.prefix, dvbi.e_VideoConformancePoint, ++cp), props.schema)) != null) - if (conf.attr(dvbi.a_href) && !this.#allowedVideoConformancePoints.isIn(conf.attr(dvbi.a_href).value())) - errs.addError({ - code: "SI091", - message: `invalid ${dvbi.a_href.attribute(dvbi.e_VideoConformancePoint)} value (${conf - .attr(dvbi.a_href) - .value()}) ${this.#allowedVideoConformancePoints.valuesRange()}`, - fragment: conf, - key: "video conf point", - }); + let codec_count = 0, + conf_points = []; + while ((conf = ContentAttributes.get(xPath(props.prefix, dvbi.e_VideoConformancePoint, ++cp), props.schema)) != null) { + if (conf.attr(dvbi.a_href)) { + let conformanceVal = conf.attr(dvbi.a_href).value(); + if (!this.#allowedVideoConformancePoints.isIn(conformanceVal)) + errs.addError({ + code: "SI091", + message: `invalid ${dvbi.a_href.attribute(dvbi.e_VideoConformancePoint)} value (${conformanceVal}) ${this.#allowedVideoConformancePoints.valuesRange()}`, + fragment: conf, + key: "video conf point", + }); + else if (!isHDRDMISystem(conformanceVal)) { + codec_count++; + if (codec_count > 1) + errs.addError({ + code: "SI092", + message: "only a single conformance point for the codec can be specified", + fragment: conf, + key: "video conf point", + }); + } + if (isIn(conf_points, conformanceVal)) + errs.addError({ + code: "SI093", + message: `duplicated value for ${dvbi.e_VideoConformancePoint.elementize()}`, + fragment: conf, + key: "duplicate conformance point", + }); + else conf_points.push(conformanceVal); + } + } // Check ContentAttributes/CaptionLanguage cp = 0; @@ -1716,17 +1497,37 @@ export default class ServiceListCheck { }); } - // - const MulticastTSDeliveryParameters = DASHDeliveryParameters.get(xPath(props.prefix, dvbi.e_MulticastTSDeliveryParameters), props.schema); - if (MulticastTSDeliveryParameters) { - checkMulticastDeliveryParams(MulticastTSDeliveryParameters, errs, "SI176"); + // -- !! EXPERIMENTAL + let cc = 0, + CMCDelem; + while ((CMCDelem = DASHDeliveryParameters.get(xPath(props.prefix, dvbi.e_CMCD, ++cc), props.schema)) != null) { + const enabledKeys = CMCDelem.attr(dvbi.a_enabledKeys); + if (enabledKeys) { + const keys = enabledKeys.value().split(" "); + if (!CMCDelem.attr(dvbi.a_contentId) && isIn(keys, CMCD_keys.content_id)) + errs.addError({ + code: "SI175", + message: `${dvbi.a_contentId.attribute()} must be specified when ${dvbi.a_enabledKeys.attribute()} contains '${CMCD_keys.content_id}'`, + fragment: CMCDelem, + key: "CMCD", + }); + else if (CMCDelem.attr(dvbi.a_contentId) && !isIn(keys, CMCD_keys.content_id)) + errs.addError({ + type: WARNING, + code: "SI176", + message: `${dvbi.a_contentId.attribute()} is specified by key '${CMCD_keys.content_id}' not requested for reporting`, + fragment: CMCDelem, + key: "CMCD", + }); + checkCMCDkeys(CMCDelem, errs, "SL177"); + } } // let e = 0, Extension; while ((Extension = DASHDeliveryParameters.get(xPath(props.prefix, dvbi.e_Extension, ++e), props.schema)) != null) { - this.#CheckExtension(Extension, EXTENSION_LOCATION_DASH_INSTANCE, errs, "SI175"); + this.#CheckExtension(Extension, EXTENSION_LOCATION_DASH_INSTANCE, errs, "SI179"); } } @@ -1781,7 +1582,7 @@ export default class ServiceListCheck { fragment: element, }); }; - let DisallowedElement = (element, childElementName, modulation, suffix="-0") => { + let DisallowedElement = (element, childElementName, modulation, suffix = "-0") => { if (hasChild(element, childElementName)) errs.addError({ code: `SI204${suffix}`, @@ -1805,7 +1606,7 @@ export default class ServiceListCheck { checkElement(ModulationType, dvbi.e_ModulationType, sats.S2_Modulation, sats.MODULATION_S2, "SI202b"); checkElement(FEC, dvbi.e_FEC, sats.S2_FEC, sats.MODULATION_S2, "SI203b"); DisallowedElement(DVBSDeliveryParameters, dvbi.e_ModcodMode, sats.MODULATION_S2, "k"); - DisallowedElement(DVBSDeliveryParameters, dvbi.e_InputStreamIdentifier, sats.MODULATION_S2, 'l'); + DisallowedElement(DVBSDeliveryParameters, dvbi.e_InputStreamIdentifier, sats.MODULATION_S2, "l"); DisallowedElement(DVBSDeliveryParameters, dvbi.e_ChannelBonding, sats.MODULATION_S2, "m"); break; case sats.MODULATION_S2X: @@ -1959,13 +1760,15 @@ export default class ServiceListCheck { let uID = service.get(xPath(props.prefix, dvbi.e_UniqueIdentifier), props.schema); if (uID) { thisServiceId = uID.text(); - if (!validServiceIdentifier(thisServiceId)) + if (!validServiceIdentifier(thisServiceId)) { errs.addError({ code: "SL110", message: `${thisServiceId.quote()} is not a valid service identifier`, fragment: uID, key: "invalid tag", }); + errs.errorDescription({ code: "SL110", description: "service identifier should be a tag: URI according to IETF RFC 4151" }); + } if (!uniqueServiceIdentifier(thisServiceId, knownServices)) errs.addError({ code: "SL111", @@ -1995,7 +1798,7 @@ export default class ServiceListCheck { type: WARNING, code: "SL131", key: "duplicate value", - message: `duplicate value (${TargetRegion.value}) specified for ${dvbi.e_TargetRegion.elementize()}`, + message: `duplicate value (${TargetRegion.text()}) specified for ${dvbi.e_TargetRegion.elementize()}`, fragment: TargetRegion, }); } @@ -2168,81 +1971,72 @@ export default class ServiceListCheck { PE, known = []; while ((PE = ProminenceList.get(xPath(props.prefix, dvbi.e_Prominence, ++p), props.schema)) != null) { - if (!PE.attr(dvbi.a_country) && !PE.attr(dvbi.a_region) && !PE.attr(dvbi.a_ranking)) { - errs.addError({ - code: "SL228", - message: `one of ${dvbi.a_country.attribute()}, ${dvbi.a_region.attribute()} or ${dvbi.a_ranking.attribute()} must be provided`, - fragment: PE, - key: "missing value", - }); - } else { - // if @region is used, it must be in the RegionList - if (PE.attr(dvbi.a_region)) { - let prominenceRegion = PE.attr(dvbi.a_region).value(); - let found = knownRegionIDs.find((r) => r.region == prominenceRegion); - if (found === undefined) - errs.addError({ - code: "SL229", - message: `regionID ${prominenceRegion.quote()} not specified in ${dvbi.e_RegionList.elementize()}`, - fragment: PE, - key: keys.k_InvalidRegion, - }); - else found.used = true; - } - // if @country and @region are used, they must be per the region list - if (PE.attr(dvbi.a_country) && PE.attr(dvbi.a_region)) { - let prominenceRegion = PE.attr(dvbi.a_region).value(); - let prominenceCountry = PE.attr(dvbi.a_country).value(); - let found = knownRegionIDs.find((r) => r.region == prominenceRegion); - if (found !== undefined && Object.prototype.hasOwnProperty.call(found, "countries")) { - if (found.countries.length) { - if (found.countries.find((c) => c == prominenceCountry) === undefined) - errs.addError({ - code: "SL230", - message: `regionID ${prominenceRegion.quote()} not specified for country ${prominenceCountry.quote()} in ${dvbi.e_RegionList.elementize()}`, - fragment: PE, - key: keys.k_InvalidRegion, - }); - else found.used = true; - } - } - } - - // if @country is specified, it must be valid - if (PE.attr(dvbi.a_country) && !this.#knownCountries.isISO3166code(PE.attr(dvbi.a_country).value())) { + // if @region is used, it must be in the RegionList + if (PE.attr(dvbi.a_region)) { + let prominenceRegion = PE.attr(dvbi.a_region).value(); + let found = knownRegionIDs.find((r) => r.region == prominenceRegion); + if (found === undefined) errs.addError({ - code: "SL244", - message: InvalidCountryCode(PE.attr(dvbi.a_country).value(), null, `service ${thisServiceId.quote()}`), + code: "SL229", + message: `regionID ${prominenceRegion.quote()} not specified in ${dvbi.e_RegionList.elementize()}`, fragment: PE, - key: keys.k_InvalidCountryCode, - }); - } - - // for exact match - let hash1 = `c:${PE.attr(dvbi.a_country) ? PE.attr(dvbi.a_country).value() : "**"} re:${PE.attr(dvbi.a_region) ? PE.attr(dvbi.a_region).value() : "**"} ra:${ - PE.attr(dvbi.a_ranking) ? PE.attr(dvbi.a_ranking).value() : "**" - }`; - if (!isIn(known, hash1)) known.push(hash1); - else { - let country = `${PE.attr(dvbi.a_country) ? `country:${PE.attr(dvbi.a_country).value}` : ""}`, - region = `${PE.attr(dvbi.a_region) ? `region:${PE.attr(dvbi.a_region).value}` : ""}`, - ranking = `${PE.attr(dvbi.a_ranking) ? `ranking:${PE.attr(dvbi.a_ranking).value}` : ""}`; - errs.addError({ - code: "SL245", - message: `duplicate ${dvbi.e_Prominence.elementize()} for ${country} ${region} ${ranking}`, - fragment: PE, - key: `duplicate ${dvbi.e_Prominence}`, + key: keys.k_InvalidRegion, }); + else found.used = true; + } + // if @country and @region are used, they must be per the region list + if (PE.attr(dvbi.a_country) && PE.attr(dvbi.a_region)) { + let prominenceRegion = PE.attr(dvbi.a_region).value(); + let prominenceCountry = PE.attr(dvbi.a_country).value(); + let found = knownRegionIDs.find((r) => r.region == prominenceRegion); + if (found !== undefined && Object.prototype.hasOwnProperty.call(found, "countries")) { + if (found.countries.length) { + if (found.countries.find((c) => c == prominenceCountry) === undefined) + errs.addError({ + code: "SL230", + message: `regionID ${prominenceRegion.quote()} not specified for country ${prominenceCountry.quote()} in ${dvbi.e_RegionList.elementize()}`, + fragment: PE, + key: keys.k_InvalidRegion, + }); + else found.used = true; + } } - // for multiple @ranking in same country/region pair + } + // if @country is specified, it must be valid + if (PE.attr(dvbi.a_country) && !this.#knownCountries.isISO3166code(PE.attr(dvbi.a_country).value())) { + errs.addError({ + code: "SL244", + message: InvalidCountryCode(PE.attr(dvbi.a_country).value(), null, `service ${thisServiceId.quote()}`), + fragment: PE, + key: keys.k_InvalidCountryCode, + }); + } + // for exact match + let hash1 = `c:${PE.attr(dvbi.a_country) ? PE.attr(dvbi.a_country).value() : "**"} re:${PE.attr(dvbi.a_region) ? PE.attr(dvbi.a_region).value() : "**"} ra:${ + PE.attr(dvbi.a_ranking) ? PE.attr(dvbi.a_ranking).value() : "**" + }`; + if (!isIn(known, hash1)) known.push(hash1); + else { + let country = `${PE.attr(dvbi.a_country) ? `country:${PE.attr(dvbi.a_country).value()}` : ""}`, + region = `${PE.attr(dvbi.a_region) ? `region:${PE.attr(dvbi.a_region).value()}` : ""}`, + ranking = `${PE.attr(dvbi.a_ranking) ? `ranking:${PE.attr(dvbi.a_ranking).value()}` : ""}`; + errs.addError({ + code: "SL245", + message: `duplicate ${dvbi.e_Prominence.elementize()} ${country.length || region.length || ranking.length ? "for" : ""} ${country} ${region} ${ranking}`, + fragment: PE, + key: `duplicate ${dvbi.e_Prominence}`, + }); + } + // for multiple @ranking in same country/region pair + if (PE.attr(dvbi.a_ranking)) { let hash2 = `c:${PE.attr(dvbi.a_country) ? PE.attr(dvbi.a_country).value() : "**"} re:${PE.attr(dvbi.a_region) ? PE.attr(dvbi.a_region).value() : "**"}`; if (!isIn(known, hash2)) known.push(hash2); else { - let country = `${PE.attr(dvbi.a_country) ? `country:${PE.attr(dvbi.a_country).value}` : ""}`, - region = `${PE.attr(dvbi.a_region) ? `region:${PE.attr(dvbi.a_region).value}` : ""}`; + let country = `${PE.attr(dvbi.a_country) ? `country:${PE.attr(dvbi.a_country).value()}` : ""}`, + region = `${PE.attr(dvbi.a_region) ? `region:${PE.attr(dvbi.a_region).value()}` : ""}`; errs.addError({ code: "SL246", - message: `multiple ${dvbi.a_ranking.attribute()} ${country || region ? "for" : ""} ${country} ${region}`, + message: `multiple ${dvbi.a_ranking.attribute()} ${country.length || region.length ? "for" : ""} ${country} ${region}`, fragment: PE, key: `duplicate ${dvbi.e_Prominence}`, }); @@ -2250,6 +2044,7 @@ export default class ServiceListCheck { } } } + // check let ParentalRating = service.get(xPath(props.prefix, "ParentalRating"), props.schema); if (ParentalRating) { @@ -2293,7 +2088,7 @@ export default class ServiceListCheck { } /*private*/ #doSchemaVerification(ServiceList, props, errs, errCode) { - let x = SchemaVersions.find((s) => s.namespace == props.namespace); + let x = GetSchema(props.namespace); if (x && x.schema) { SchemaCheck(ServiceList, x.schema, errs, `${errCode}:${SchemaVersion(props.namespace)}`); SchemaVersionCheck(props, ServiceList, x.status, errs, `${errCode}a`); @@ -2309,7 +2104,7 @@ export default class ServiceListCheck { * @param {Class} errs Errors found in validaton * @param {String} log_prefix the first part of the logging location (or null if no logging) */ - /*public*/ doValidateServiceList(SLtext, errs, log_prefix=null) { + /*public*/ doValidateServiceList(SLtext, errs, log_prefix = null) { this.#numRequests++; if (!SLtext) { errs.addError({ @@ -2682,13 +2477,20 @@ export default class ServiceListCheck { key: "undefined region", }); else { - if (foundRegion.selectable == false) + if (foundRegion.selectable == false) { errs.addError({ code: "SL242", message: `${dvbi.e_TargetRegion.elementize()} ${TargetRegion.text().quote()} in ${dvbi.e_LCNTable.elementize()} is not selectable`, fragment: TargetRegion, key: "unselectable region", }); + errs.errorDescription({ + code: "SL242", + description: `the region ID specified in the ${dvbi.e_TargetRegion.elementize()} is defined with ${ + dvbi.a_selectable + }=false in the ${dvbi.e_RegionList.elementize()} `, + }); + } foundRegion.used = true; } @@ -2821,7 +2623,7 @@ export default class ServiceListCheck { errs.errorDescription({ code: "SL282", clause: "see A177 table 14", - description: `lanugages used in ${tva.e_AudioAttributes.elementize()}${tva.e_AudioLanguage.elementize()} should be announced in ${dvbi.e_LanguageList.elementize()}`, + description: `only lanugages used in ${tva.e_AudioAttributes.elementize()}${tva.e_AudioLanguage.elementize()} should be announced in ${dvbi.e_LanguageList.elementize()}`, }); } }); diff --git a/sl_data_versions.js b/sl_data_versions.js new file mode 100644 index 0000000..74bfd46 --- /dev/null +++ b/sl_data_versions.js @@ -0,0 +1,333 @@ +/** + * sl_data_versions.js + * + * version related checks (lifted from sl-check.js) + */ + +import { readFileSync } from "fs"; +import process from "process"; + +import chalk from "chalk"; +import { parseXmlString } from "libxmljs2"; + +import { OLD, DRAFT, ETSI, CURRENT } from "./globals.js"; +import { dvbi } from "./DVB-I_definitions.js"; +import { DVBI_ServiceListSchema, __dirname_linux } from "./data_locations.js"; + +export const ANY_NAMESPACE = "$%$!!"; + +export const SCHEMA_r0 = 0, + SCHEMA_r1 = 1, + SCHEMA_r2 = 2, + SCHEMA_r3 = 3, + SCHEMA_r4 = 4, + SCHEMA_r5 = 5, + SCHEMA_r6 = 6, + SCHEMA_r7 = 7, + SCHEMA_unknown = -1; + +let SchemaVersions = [ + // schema property is loaded from specified filename + { + namespace: dvbi.A177r7_Namespace, + version: SCHEMA_r7, + filename: DVBI_ServiceListSchema.r7.file, + schema: null, + status: DRAFT, + specVersion: "A177r7", + }, + { + namespace: dvbi.A177r6_Namespace, + version: SCHEMA_r6, + filename: DVBI_ServiceListSchema.r6.file, + schema: null, + status: CURRENT, + specVersion: "A177r6", + }, + { + namespace: dvbi.A177r5_Namespace, + version: SCHEMA_r5, + filename: DVBI_ServiceListSchema.r5.file, + schema: null, + status: OLD, + specVersion: "A177r5", + }, + { + namespace: dvbi.A177r4_Namespace, + version: SCHEMA_r4, + filename: DVBI_ServiceListSchema.r4.file, + schema: null, + status: OLD, + specVersion: "A177r4", + }, + { + namespace: dvbi.A177r3_Namespace, + version: SCHEMA_r3, + filename: DVBI_ServiceListSchema.r3.file, + schema: null, + status: OLD, + specVersion: "A177r3", + }, + { + namespace: dvbi.A177r2_Namespace, + version: SCHEMA_r2, + filename: DVBI_ServiceListSchema.r2.file, + schema: null, + status: OLD, + specVersion: "A177r2", + }, + { + namespace: dvbi.A177r1_Namespace, + version: SCHEMA_r1, + filename: DVBI_ServiceListSchema.r1.file, + schema: null, + status: ETSI, + specVersion: "A177r1", + }, + { + namespace: dvbi.A177_Namespace, + version: SCHEMA_r0, + filename: DVBI_ServiceListSchema.r0.file, + schema: null, + status: OLD, + specVersion: "A177", + }, +]; + +const OutOfScheduledHoursBanners = [ + { ver: SCHEMA_r7, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v3 }, + { ver: SCHEMA_r6, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v3 }, + { ver: SCHEMA_r5, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v3 }, + { ver: SCHEMA_r4, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v3 }, + { ver: SCHEMA_r3, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v3 }, + { ver: SCHEMA_r2, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v2 }, + { ver: SCHEMA_r1, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v2 }, + { ver: SCHEMA_r0, val: dvbi.BANNER_OUTSIDE_AVAILABILITY_v1 }, +]; +const ContentFinishedBanners = [ + { ver: SCHEMA_r7, val: dvbi.BANNER_CONTENT_FINISHED_v3 }, + { ver: SCHEMA_r6, val: dvbi.BANNER_CONTENT_FINISHED_v3 }, + { ver: SCHEMA_r5, val: dvbi.BANNER_CONTENT_FINISHED_v3 }, + { ver: SCHEMA_r4, val: dvbi.BANNER_CONTENT_FINISHED_v3 }, + { ver: SCHEMA_r3, val: dvbi.BANNER_CONTENT_FINISHED_v3 }, + { ver: SCHEMA_r2, val: dvbi.BANNER_CONTENT_FINISHED_v2 }, + { ver: SCHEMA_r1, val: dvbi.BANNER_CONTENT_FINISHED_v2 }, +]; +const ServiceListLogos = [ + { ver: SCHEMA_r7, val: dvbi.LOGO_SERVICE_LIST_v3 }, + { ver: SCHEMA_r6, val: dvbi.LOGO_SERVICE_LIST_v3 }, + { ver: SCHEMA_r5, val: dvbi.LOGO_SERVICE_LIST_v3 }, + { ver: SCHEMA_r4, val: dvbi.LOGO_SERVICE_LIST_v3 }, + { ver: SCHEMA_r3, val: dvbi.LOGO_SERVICE_LIST_v3 }, + { ver: SCHEMA_r2, val: dvbi.LOGO_SERVICE_LIST_v2 }, + { ver: SCHEMA_r1, val: dvbi.LOGO_SERVICE_LIST_v2 }, + { ver: SCHEMA_r0, val: dvbi.LOGO_SERVICE_LIST_v1 }, +]; +const ServiceLogos = [ + { ver: SCHEMA_r7, val: dvbi.LOGO_SERVICE_v3 }, + { ver: SCHEMA_r6, val: dvbi.LOGO_SERVICE_v3 }, + { ver: SCHEMA_r5, val: dvbi.LOGO_SERVICE_v3 }, + { ver: SCHEMA_r4, val: dvbi.LOGO_SERVICE_v3 }, + { ver: SCHEMA_r3, val: dvbi.LOGO_SERVICE_v3 }, + { ver: SCHEMA_r2, val: dvbi.LOGO_SERVICE_v2 }, + { ver: SCHEMA_r1, val: dvbi.LOGO_SERVICE_v2 }, + { ver: SCHEMA_r0, val: dvbi.LOGO_SERVICE_v1 }, +]; +const ServiceBanners = [ + { ver: SCHEMA_r7, val: dvbi.SERVICE_BANNER_v4 }, + { ver: SCHEMA_r6, val: dvbi.SERVICE_BANNER_v4 }, + { ver: SCHEMA_r5, val: dvbi.SERVICE_BANNER_v4 }, + { ver: SCHEMA_r4, val: dvbi.SERVICE_BANNER_v4 }, + { ver: SCHEMA_r3, val: dvbi.SERVICE_BANNER_v4 }, + { ver: SCHEMA_r2, val: dvbi.SERVICE_BANNER_v4 }, +]; +const ContentGuideSourceLogos = [ + { ver: SCHEMA_r7, val: dvbi.LOGO_CG_PROVIDER_v3 }, + { ver: SCHEMA_r6, val: dvbi.LOGO_CG_PROVIDER_v3 }, + { ver: SCHEMA_r5, val: dvbi.LOGO_CG_PROVIDER_v3 }, + { ver: SCHEMA_r4, val: dvbi.LOGO_CG_PROVIDER_v3 }, + { ver: SCHEMA_r3, val: dvbi.LOGO_CG_PROVIDER_v3 }, + { ver: SCHEMA_r2, val: dvbi.LOGO_CG_PROVIDER_v2 }, + { ver: SCHEMA_r1, val: dvbi.LOGO_CG_PROVIDER_v2 }, + { ver: SCHEMA_r0, val: dvbi.LOGO_CG_PROVIDER_v1 }, +]; + +/** + * determine the schema version (and hence the specificaion version) in use + * + * @param {String} namespace The namespace used in defining the schema + * @returns {integer} Representation of the schema version or error code if unknown + */ +export let SchemaVersion = (namespace) => { + const x = SchemaVersions.find((ver) => ver.namespace == namespace); + return x ? x.version : SCHEMA_unknown; +}; + +export let SchemaSpecVersion = (namespace) => { + const x = SchemaVersions.find((ver) => ver.namespace == namespace); + return x ? x.specVersion : "r?"; +}; + +export let GetSchema = (namespace) => SchemaVersions.find((s) => s.namespace == namespace); + +/** + * determines if the identifer provided refers to a valid application being used with the service + * + * @param {String} hrefType The type of the service application + * @param {integer} schemaVersion The schema version of the XML document + * @returns {boolean} true if this is a valid application being used with the service else false + */ +export let validServiceControlApplication = (hrefType, schemaVersion) => { + let appTypes = [dvbi.APP_IN_PARALLEL, dvbi.APP_IN_CONTROL]; + if (schemaVersion >= SCHEMA_r6) appTypes.push(dvbi.APP_SERVICE_PROVIDER); + if (schemaVersion >= SCHEMA_r7) appTypes.push(dvbi.APP_IN_SERIES); + return appTypes.includes(hrefType); +}; + +/** + * determines if the identifer provided refers to a validconsent application type + * + * @param {String} hrefType The type of the service application + * @param {integer} schemaVersion The schema version of the XML document + * @returns {boolean} true if this is a valid application being used with the service else false + */ +export let validAgreementApplication = (hrefType, schemaVersion) => { + let appTypes = [dvbi.APP_LIST_INSTALLATION, dvbi.APP_WITHDRAW_AGREEMENT, dvbi.APP_RENEW_AGREEMENT]; + return schemaVersion >= SCHEMA_r7 && appTypes.includes(hrefType); +}; + +/** + * determines if the identifer provided refers to a valid application being used with the service instance + * + * @param {String} hrefType The type of the service application + * @param {integer} schemaVersion The schema version of the XML document + * @returns {boolean} true if this is a valid application being used with the service else false + */ +export let validServiceInstanceControlApplication = (hrefType, schemaVersion) => { + let appTypes = [dvbi.APP_IN_PARALLEL, dvbi.APP_IN_CONTROL]; + if (schemaVersion >= SCHEMA_r7) appTypes.push(dvbi.APP_IN_SERIES); + return appTypes.includes(hrefType); +}; + +/** + * determines if the identifer provided refers to a valid application to be launched when a service is unavailable + * + * @param {String} hrefType The type of the service application + * @returns {boolean} true if this is a valid application to be launched when a service is unavailable else false + */ +export let validServiceUnavailableApplication = (hrefType) => hrefType == dvbi.APP_OUTSIDE_AVAILABILITY; + +/** + * determines if the identifer provided refers to a valid DASH media type (single MPD or MPD playlist) + * per A177 clause 5.2.7.2 + * + * @param {String} contentType The contentType for the file + * @returns {boolean} true if this is a valid MPD or playlist identifier + */ +export let validDASHcontentType = (contentType) => [dvbi.CONTENT_TYPE_DASH_MPD, dvbi.CONTENT_TYPE_DVB_PLAYLIST].includes(contentType); + +/** + * looks for the {index, value} pair within the array of permitted values + * + * @param {array} permittedValues array of allowed value pairs {ver: , val:} + * @param {any} value value to match with val: in the allowed values + * @param {any} version value to match with ver: in the allowed values or ANY_NAMESPACE + * @returns {boolean} true if {index, value} pair exists in the list of allowed values when namespace is specific or if any val: equals value with namespace is ANY_NAMESPACE, else false + */ +function match(permittedValues, value, version = ANY_NAMESPACE) { + if (permittedValues && value) { + if (version == ANY_NAMESPACE) return permittedValues.find((elem) => elem.val == value) != undefined; + else { + let _ver = SchemaVersion(version); + let i = permittedValues.find((elem) => elem.ver == _ver); + return i && i.val == value; + } + } + return false; +} + +/** + * determines if the identifer provided refers to a valid banner for out-of-servce-hours presentation + * + * @param {XMLnode} HowRelated The banner identifier + * @param {String} namespace The namespace being used in the XML document + * @returns {boolean} true if this is a valid banner for out-of-servce-hours presentation else false + */ +export function validOutScheduleHours(HowRelated, namespace) { + // return true if val is a valid CS value for Out of Service Banners (A177 5.2.5.3) + return match(OutOfScheduledHoursBanners, HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : null, namespace); +} + +/** + * determines if the identifer provided refers to a valid banner for content-finished presentation + * + * @since DVB A177r1 + * @param {XMLnode} HowRelated The banner identifier + * @param {String} namespace The namespace being used in the XML document + * @returns {boolean} true if this is a valid banner for content-finished presentation else false + */ +export function validContentFinishedBanner(HowRelated, namespace) { + // return true if val is a valid CS value for Content Finished Banner (A177 5.2.7.3) + return match(ContentFinishedBanners, HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : null, namespace); +} + +/** + * determines if the identifer provided refers to a valid service list logo + * + * @param {XMLnode} HowRelated The logo identifier + * @param {String} namespace The namespace being used in the XML document + * @returns {boolean} true if this is a valid logo for a service list else false + */ +export function validServiceListLogo(HowRelated, namespace) { + // return true if HowRelated@href is a valid CS value Service List Logo (A177 5.2.6.1) + return match(ServiceListLogos, HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : null, namespace); +} + +export function validServiceAgreementApp(HowRelated, namespace) { + return HowRelated.attr(dvbi.a_href) ? validAgreementApplication(HowRelated.attr(dvbi.a_href).value(), SchemaVersion(namespace)) : false; +} + +/** + * determines if the identifer provided refers to a valid service logo + * + * @param {XMLnode} HowRelated The logo identifier + * @param {String} namespace The namespace being used in the XML document + * @returns {boolean} true if this is a valid logo for a service else false + */ +export function validServiceLogo(HowRelated, namespace) { + // return true if val is a valid CS value Service Logo (A177 5.2.6.2) + return match(ServiceLogos, HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : null, namespace); +} + +/** + * determines if the identifer provided refers to a valid service banner + * + * @param {XMLnode} HowRelated The logo identifier + * @param {String} namespace The namespace being used in the XML document + * @returns {boolean} true if this is a valid banner for a service else false + */ +export function validServiceBanner(HowRelated, namespace) { + // return true if val is a valid CS value Service Banner (A177 5.2.6.x) + return match(ServiceBanners, HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : null, namespace); +} + +/** + * determines if the identifer provided refers to a valid content guide source logo + * + * @param {XMLnode} HowRelated The logo identifier + * @param {String} namespace The namespace being used in the XML document + * @returns {boolean} true if this is a valid logo for a content guide source else false + */ +export function validContentGuideSourceLogo(HowRelated, namespace) { + // return true if val is a valid CS value Service Logo (A177 5.2.6.3) + return match(ContentGuideSourceLogos, HowRelated.attr(dvbi.a_href) ? HowRelated.attr(dvbi.a_href).value() : nul, namespace); +} + +// TODO - change this to support sync/async and file/url reading +console.log(chalk.yellow.underline("loading service list schemas...")); +SchemaVersions.forEach((version) => { + process.stdout.write(chalk.yellow(`..loading ${version.version} ${version.namespace} from ${version.filename} `)); + let schema = readFileSync(version.filename).toString().replace(`schemaLocation="./`, `schemaLocation="${__dirname_linux}/`); + version.schema = parseXmlString(schema); + console.log(version.schema ? chalk.green("OK") : chalk.red.bold("FAIL")); +}); diff --git a/slepr.js b/slepr.js index dde615c..7b6a26c 100644 --- a/slepr.js +++ b/slepr.js @@ -1,6 +1,6 @@ /** * slepr.js - * + * * SLEPR - Service List End Point Resolver */ import { readFile } from "fs"; @@ -13,7 +13,7 @@ import { datatypeIs } from "./phlib/phlib.js"; import { tva } from "./TVA_definitions.js"; import { dvbi } from "./DVB-I_definitions.js"; -import { handleErrors } from "./fetch_err_handler.js"; +import handleErrors from "./fetch_err_handler.js"; import { xPath, isIn } from "./utils.js"; import { IANA_Subtag_Registry, ISO3166, TVA_ContentCS, TVA_FormatCS, DVBI_ContentSubject } from "./data_locations.js"; import { hasChild } from "./schema_checks.js"; @@ -23,7 +23,7 @@ import ClassificationScheme from "./classification_scheme.js"; import ISOcountries from "./ISO_countries.js"; var masterSLEPR = ""; -const EMPTY_SLEPR = ''; +const EMPTY_SLEPR = ''; const RFC2397_PREFIX = "data:"; @@ -142,13 +142,9 @@ export default class SLEPR { // check for any erronous arguments for (var key in req.query) if (!isIn(allowed_arguments, key, false)) req.parseErr.push(`invalid argument [${key}]`); - //regulatorListFlag needs to be a boolean, "true" or "false" only - if (req.query.regulatorListFlag) { - if (!datatypeIs(req.query.regulatorListFlag, "string")) req.parseErr.push(`invalid type for ${dvbi.a_regulatorListFlag} [${typeof req.query.regulatorListFlag}]`); - - if (!["true", "false"].includes(req.query.regulatorListFlag.toLowerCase())) - req.parseErr.push(`invalid value for ${dvbi.a_regulatorListFlag} [${req.query.regulatorListFlag}]`); - } + var checkBoolean = (bool) => ["true", "false"].includes(bool); + checkIt(req.query.regulatorListFlag, dvbi.a_regulatorListFlag, checkBoolean); + checkIt(req.query.inlineImages, dvbi.q_inlineImages, checkBoolean); //TargetCountry(s) var checkTargetCountry = (country) => this.#knownCountries.isISO3166code(country, false); @@ -186,10 +182,9 @@ export default class SLEPR { res.type("text/plain"); res.write("urn:paulhiggs,2024-06:BabelFish#ja,zh,mi\nurn:ibm.com,1981:CTrlAtlDel\n"); res.status(200); - return true; + return true; } - if (!this.checkQuery(req)) { if (req.parseErr) res.write(`[${req.parseErr.join(",\n\t")}]`); res.status(400); diff --git a/test/input/test-001/test-re2.js b/test/input/test-001/test-re2.js index 77aa801..620d038 100644 --- a/test/input/test-001/test-re2.js +++ b/test/input/test-001/test-re2.js @@ -15,14 +15,14 @@ import { validZuluTimeType, isUTCDateTime, } from "../../../pattern_checks.js"; + import {isTAGURI} from "../../../URI_checks.js"; -const AVCregex = /[a-z0-9!\"#$%&'()*+,./:;<=>?@[\] ^_`{|}~-]{4}\.[a-f0-9]{6}/i; +const AVCregex = /[a-z0-9!"#$%&'()*+,./:;<=>?@[\] ^_`{|}~-]{4}\.[a-f0-9]{6}/i; const AC4regex = /ac-4(\.[a-fA-F\d]{1,2}){3}/; const VP9regex = /^vp09(\.\d{2}){3}(\.(\d{2})?){0,5}$/; const AV1regex = /^av01\.\d\.\d+[MH]\.\d{1,2}((\.\d?)(\.(\d{3})?(\.(\d{2})?(.(\d{2})?(.(\d{2})?(.\d?)?)?)?)?)?)?$/; - const ConsoleColours = { Reset: "\x1b[0m", Bright: "\x1b[1m", diff --git a/ui.js b/ui.js index 5b27e19..a785532 100644 --- a/ui.js +++ b/ui.js @@ -1,18 +1,23 @@ /** * ui.js - * + * * Drive the HTML user interface */ import { readFileSync } from "fs"; import { HTMLize } from "./phlib/phlib.js"; import { ERROR, WARNING } from "./error_list.js"; -import { MODE_URL, MODE_FILE } from "./validator.js"; + +export const MODE_UNSPECIFIED = "none", + MODE_SL = "sl", + MODE_CG = "cg", + MODE_URL = "url", + MODE_FILE = "file"; const MESSAGES_IN_ORDER = true; // when true outputs the errors, warnings and informations in the 'document order'. false==ouotput in order found const SHOW_LINE_NUMBER = false; // include the line number in the XML document where the error was found -let pkg = JSON.parse(readFileSync("./package.json", { encoding: "utf-8" } ).toString()); +let pkg = JSON.parse(readFileSync("./package.json", { encoding: "utf-8" }).toString()); export function PAGE_TOP(pageTitle, label = null) { const TABLE_STYLE = @@ -69,7 +74,10 @@ function tabulateResults(source, res, error, errs) { return res.write(`${HTMLize(i)}${errs.countsErr[i]}`); }); Object.keys(errs.countsWarn).forEach(function (i) { - return res.write(`${HTMLize(i)}${errs.countsWarn[i]}`); + return res.write(`W: ${HTMLize(i)}${errs.countsWarn[i]}`); + }); + Object.keys(errs.countsInfo).forEach(function (i) { + return res.write(`I: ${HTMLize(i)}${errs.countsInfo[i]}`); }); resultsShown = true; res.write(TABLE_FOOTER); @@ -127,7 +135,7 @@ function tabulateResults(source, res, error, errs) { WARN = "warnings", INFO = "info", style = (name, colour) => ``; - res.write(`${style(ERR, "red")}${style(WARN, "blue")}${style(INFO, "green")}
`);
+		res.write(`${style(ERR, "red")}${style(WARN, "blue")}${style(INFO, "orange")}
`);
 		errs.markupXML.forEach((line) => {
 			let cla = "",
 				tip = line.validationErrors ? line.validationErrors.map((err) => HTMLize(err)).join("
") : null;
@@ -208,10 +216,9 @@ export function drawForm(deprecateTo, req, res, modes, supportedRequests, error
 export function drawResults(req, res, error = null, errs = null) {
 	res.setHeader("Content-Type", "text/html");
 	res.write(PAGE_TOP("DVB-I Validator", "DVB-I Validator"));
-	tabulateResults(req.query.url? req.query.url : "uploaded list" , res, error, errs);
+	tabulateResults(req.query.url ? req.query.url : "uploaded list", res, error, errs);
 	res.write(PAGE_BOTTOM);
 	return new Promise((resolve, /* eslint-disable no-unused-vars */ reject /* eslint-enable */) => {
 		resolve(res);
 	});
-
-}
\ No newline at end of file
+}
diff --git a/utils.js b/utils.js
index 31e203f..142408a 100644
--- a/utils.js
+++ b/utils.js
@@ -1,6 +1,6 @@
 /**
  * utils.js
- * 
+ *
  * some usefule utility functions that may be used by more than one class
  */
 import { statSync, readFileSync } from "fs";
@@ -34,7 +34,7 @@ export function getElementByTagName(element, childElementName, index = null) {
 			if (!index) return getFirstElementByTagName(element, childElementName);
 
 			let cnt = 0;
-		  const ch1 = element.childNodes();
+			const ch1 = element.childNodes();
 			for (let i = 0; i < ch1.length; i++) {
 				if (ch1[i].type() == "element" && ch1[i].name() == childElementName) cnt++;
 				if (cnt >= index) return ch1[i];
@@ -200,3 +200,9 @@ export function DuplicatedValue(found, val) {
 	if (!f) found.push(val);
 	return f;
 }
+
+export function DumpString(str) {
+	let t = [];
+	for (let i = 0; i < str.length; i++) t.push(str.charCodeAt(i).toString(16));
+	return `"${str}" --> ${t.join(" ")}`;
+}
diff --git a/validator.js b/validator.js
index 4465e36..b6fcc1d 100644
--- a/validator.js
+++ b/validator.js
@@ -1,10 +1,9 @@
 /**
  * validator.js
- * 
- * 
+ *
+ *
  */
-import { existsSync, writeFile } from "fs";
-import { join, sep } from "path";
+import { join } from "path";
 import { createServer } from "https";
 import os from "node:os";
 import process from "process";
@@ -40,52 +39,14 @@ import {
 import ServiceListCheck from "./sl_check.js";
 import ContentGuideCheck from "./cg_check.js";
 import SLEPR from "./slepr.js";
-
-export const MODE_UNSPECIFIED = "none",
-	MODE_SL = "sl",
-	MODE_CG = "cg",
-	MODE_URL = "url",
-	MODE_FILE = "file";
+import writeOut, { createPrefix } from "./logger.js";
+import { MODE_URL, MODE_FILE, MODE_SL, MODE_CG, MODE_UNSPECIFIED } from "./ui.js";
 
 let csr = null;
 
 const keyFilename = join(".", "selfsigned.key"),
 	certFilename = join(".", "selfsigned.crt");
 
-export function writeOut(errs, filebase, markup, req = null) {
-	if (!filebase || errs.markupXML?.length == 0) return;
-
-	let outputLines = [];
-	if (markup && req?.body?.XMLurl) outputLines.push(``);
-	errs.markupXML.forEach((line) => {
-		outputLines.push(line.value);
-		if (markup && line.validationErrors)
-			line.validationErrors.forEach((error) => {
-				outputLines.push(``);
-			});
-	});
-	const filename = markup ? `${filebase}.mkup.txt` : `${filebase}.raw.txt`;
-	writeFile(filename, outputLines.join("\n"), (err) => {
-		if (err) console.log(chalk.red(err));
-	});
-}
-
-function createPrefix(req) {
-	const logDir = join(".", "arch");
-
-	if (!existsSync(logDir)) return null;
-
-	const getDate = (d) => {
-		const fillZero = (t) => (t < 10 ? `0${t}` : t);
-		return `${d.getFullYear()}-${fillZero(d.getMonth() + 1)}-${fillZero(d.getDate())} ${fillZero(d.getHours())}.${fillZero(d.getMinutes())}.${fillZero(d.getSeconds())}`;
-	};
-
-	const fname = req.body.doclocation == MODE_URL ? req.body.XMLurl.substr(req.body.XMLurl.lastIndexOf("/") + 1) : req?.files?.XMLfile?.name;
-	if (!fname) return null;
-
-	return `${logDir}${sep}${getDate(new Date())} (${req.body.testtype == MODE_SL ? "SL" : req.body.requestType}) ${fname.replace(/[/\\?%*:|"<>]/g, "-")}`;
-}
-
 function DVB_I_check(deprecationWarning, req, res, slcheck, cgcheck, hasSL, hasCG, mode = MODE_UNSPECIFIED, linktype = MODE_UNSPECIFIED) {
 	if (!req.session.data) {
 		// setup defaults
@@ -111,7 +72,7 @@ function DVB_I_check(deprecationWarning, req, res, slcheck, cgcheck, hasSL, hasC
 		else if (req.body.doclocation == MODE_URL && req.body.XMLurl.length == 0) req.parseErr = "URL not specified";
 		else if (req.body.doclocation == MODE_FILE && !(req.files && req.files.XMLfile)) req.parseErr = "File not provided";
 
-		let log_prefix = createPrefix(req, res);
+		const log_prefix = createPrefix(req);
 		if (!req.parseErr)
 			switch (req.body.doclocation) {
 				case MODE_URL:
@@ -168,9 +129,10 @@ function DVB_I_check(deprecationWarning, req, res, slcheck, cgcheck, hasSL, hasC
 
 function validateServiceList(req, res, slcheck) {
 	let errs = new ErrorList();
-	let resp, VVxml = null;
-	const log_prefix = createPrefix(req, res);
-	if(req.method == "GET") {
+	let resp,
+		VVxml = null;
+	const log_prefix = createPrefix(req);
+	if (req.method == "GET") {
 		try {
 			resp = fetchS(req.query.url);
 		} catch (error) {
@@ -180,9 +142,8 @@ function validateServiceList(req, res, slcheck) {
 			if (resp.ok) VVxml = resp.text();
 			else req.parseErr = `error (${resp.status}:${resp.statusText}) handling ${req.body.XMLurl}`;
 		}
-	}
-	else if(req.method == "POST") {
-		VVxml = req.body
+	} else if (req.method == "POST") {
+		VVxml = req.body;
 	}
 	slcheck.doValidateServiceList(VVxml, errs, log_prefix);
 	drawResults(req, res, req.parseErr, errs);
@@ -193,9 +154,10 @@ function validateServiceList(req, res, slcheck) {
 
 function validateServiceListJson(req, res, slcheck) {
 	let errs = new ErrorList();
-	let resp, VVxml = null;
-	const log_prefix = createPrefix(req, res);
-	if(req.method == "GET") {
+	let resp,
+		VVxml = null;
+	const log_prefix = createPrefix(req);
+	if (req.method == "GET") {
 		try {
 			resp = fetchS(req.query.url);
 		} catch (error) {
@@ -205,16 +167,18 @@ function validateServiceListJson(req, res, slcheck) {
 			if (resp.ok) VVxml = resp.text();
 			else req.parseErr = `error (${resp.status}:${resp.statusText}) handling ${req.body.XMLurl}`;
 		}
-	}
-	else if(req.method == "POST") {
+	} else if (req.method == "POST") {
 		VVxml = req.body;
 	}
 	slcheck.doValidateServiceList(VVxml, errs, log_prefix);
 	res.setHeader("Content-Type", "application/json");
-	if (req.parseErr) 
-		res.write(JSON.stringify({parseErr:req.parseErr}));
-	else 
-		res.write(JSON.stringify((req.query.results && req.query.results == "all") ? {errs} : {errors: errs.errors.length, warnings : errs.warnings.length, informationals: errs.informationals.length}));
+	if (req.parseErr) res.write(JSON.stringify({ parseErr: req.parseErr }));
+	else
+		res.write(
+			JSON.stringify(
+				req.query.results && req.query.results == "all" ? { errs } : { errors: errs.errors.length, warnings: errs.warnings.length, informationals: errs.informationals.length }
+			)
+		);
 
 	writeOut(errs, log_prefix, true, req);
 	res.end();
@@ -288,7 +252,7 @@ export default function validator(options) {
 
 	app.use(morgan(":remote-addr :protocol :method :url :status :res[content-length] :counts - :response-time ms :agent :parseErr :location"));
 
-	app.use(express.urlencoded({extended: true}));
+	app.use(express.urlencoded({ extended: true }));
 
 	app.set("trust proxy", 1);
 	app.use(
@@ -296,7 +260,7 @@ export default function validator(options) {
 			secret: "keyboard car",
 			resave: false,
 			saveUninitialized: true,
-			cookie: {maxAge: 60000},
+			cookie: { maxAge: 60000 },
 		})
 	);
 
@@ -369,12 +333,12 @@ export default function validator(options) {
 			validateServiceListJson(req, res, slcheck);
 		});
 
-		app.post("/validate_sl",express.text({type: "application/xml", limit: '2mb'}), function (req, res) {
-			validateServiceList(req, res,slcheck);
+		app.post("/validate_sl", express.text({ type: "application/xml", limit: "2mb" }), function (req, res) {
+			validateServiceList(req, res, slcheck);
 		});
 
-		app.post("/validate_sl_json",express.text({type: "application/xml", limit: '2mb'}), function (req, res) {
-			validateServiceListJson(req, res,slcheck);
+		app.post("/validate_sl_json", express.text({ type: "application/xml", limit: "2mb" }), function (req, res) {
+			validateServiceListJson(req, res, slcheck);
 		});
 	}