initial commit

This commit is contained in:
Pierre
2026-01-11 17:26:49 +01:00
commit 888790fd11
240 changed files with 48691 additions and 0 deletions
@@ -0,0 +1,50 @@
import {describe, beforeEach, it} from 'mocha';
import {expect} from 'chai';
import {from, MetadataNode} from './MetaDataAPI.js';
import {VideoMeta} from "./metadata-type/VideoMeta";
import {GenreMeta} from "./metadata-type/GenreMeta";
type TypedMetaData = VideoMeta & GenreMeta;
describe('MetaData', () => {
let root: MetadataNode<TypedMetaData>;
let meta = {};
beforeEach(() => {
// Reset meta for each test to ensure no test pollution
meta = {};
root = from<TypedMetaData>(meta);
});
it('should set and get a simple key-value pair', () => {
root.at('originalTitle').set("Star Wars");
expect(root.at('originalTitle').get()).to.equal("Star Wars");
});
it('should set and get nested metadata using nested keys', () => {
root.at('titles').at('eng').set("Star Wars");
root.at('titles').at('jpn').set("Staru Warsu");
expect(root.at('titles').at('eng').get()).to.equal("Star Wars");
expect(root.at('titles').at('jpn').get()).to.equal("Staru Warsu");
expect(root.at('titles').get()).to.deep.equal({eng: "Star Wars", jpn: "Staru Warsu"});
expect(root.get()).to.deep.equal({
titles: {eng: "Star Wars", jpn: "Staru Warsu"}
});
});
it('should return undefined for non-existent keys', () => {
expect(root.at<any>('nonexistent').at<any>('nonexistent2').get()).to.equal(undefined);
});
it('should handle overriding existing keys with new values', () => {
root.at('genres').add("Sci-fi");
expect(root.at('genres').getKeysAsSet()).to.deep.equal(new Set(["Sci-fi"]));
// Override the value
root.at('genres').add("Adventure");
expect(root.at('genres').getKeysAsSet()).to.deep.equal(new Set(['Sci-fi', 'Adventure']));
root.at('genres').add("Sci-fi");
expect(root.at('genres').getKeysAsSet()).to.deep.equal(new Set(['Sci-fi', 'Adventure']));
//root.at('genres').remove('Sci-fi');
//expect(root.at('genres').getKeys()).to.deep.equal(new Set(['Adventure']));
});
});
@@ -0,0 +1,115 @@
export function from<T>(metadata: T): MetadataNode<T> {
return new MetadataNode<T>([], metadata);
}
export class MetadataNode<T> {
constructor(private path: string[], private rootData: any, private replacementRule?: (oldValue: T, newValue: T) => T) {
this.replacementRule = replacementRule || ((oldValue: T, newValue: T) => {
console.warn(`Key ${path.join('.')} already exists and no replacement rule defined (default to taking the new one).
Current value: ${oldValue}
New value: ${newValue}`);
return newValue;
});
}
// Navigate to a subnode without ensuring it exists for further operations
at<K extends keyof T>(key: K): MetadataNode<T[K]> {
if (!key) {
throw new Error('Key is required');
}
const newPath = [...this.path, key as string];
return new MetadataNode<T[K]>(newPath, this.rootData);
}
// Set the value at the current node, ensuring the path exists
set(value: T): void {
if (value === undefined || value === null) {
//we don't want to set undefined or null values
return;
}
const current = this.ensurePath(true);
if (this.path.length > 0) {
const lastKey = this.path[this.path.length - 1];
if (current[lastKey]) {
if (current[lastKey] === value) {
return;
}
current[lastKey] = this.replacementRule(current[lastKey], value);
} else {
current[lastKey] = value;
}
}
}
// Get the value at the current node
get(defaultValue?: T): T {
const current = this.ensurePath(false);
return current === undefined ? defaultValue : current;
}
// this is used to define set of values
add(key: string): void {
if (key === undefined || key === null) {
//we don't want to set undefined or null values
return;
}
let node = this.get();
if (node && node instanceof Set) {
const set = node as Set<string>;
if (set.has(key)) {
return;
} else {
set.add(key);
}
} else {
this.set(new Set([key]) as any);
}
}
// Remove a key from the node
/*remove<K extends keyof T>(key: K|string): void {
const current = this.ensurePath(false);
if (current && current[key] !== undefined) {
delete current[key];
}
}*/
// Get all keys at the current node
getKeysAsSet(): Set<string> {
const current = this.ensurePath(false);
if(current instanceof Set){
return current as Set<string>;
}else {
return new Set(current ? Object.keys(current) as Array<string> : []);
}
}
getKeysAsArray(): string[] {
const current = this.ensurePath(false);
if(!current){
return [];
}
if(current instanceof Set){
return Array.from(current as Set<string>);
}else {
return Object.keys(current);
}
}
private ensurePath(createPath: boolean): any {
let current = this.rootData;
const path = createPath ? this.path.slice(0, -1) : this.path;
for (const segment of path) {
if (current[segment] === undefined) {
if (createPath) {
current[segment] = {};
} else {
return undefined; // Return undefined early if the path segment doesn't exist and we're not creating it
}
}
current = current[segment];
}
return current;
}
}
@@ -0,0 +1,74 @@
export const FileMetaDataName = [
"fileType",
"mimeType",
"byteSize",
]
export const hashMetaDataName = [
"cid_crc32",
"cid_md5",
"cid_sha1",
"cid_sha2-256",
"cid_sha3-256",
"cid_sha3_384",
];
export const AnimeMetaData = [
"anime"
];
export const FFMpegMetaData = [
"fileinfo"
];
export const FileNameMetaData = [
"fileName",
"extension",
"videoType",
"extra",
"season",
"episode",
"increment",
"movieYear",
"title",
"titles",
];
export const TorrentMetaData = [
"trakers"
];
export const VideoMetaData = [
"subtitles"
];
export const JellyfinMetaData = [
"videoType", // Noted as optional and with specific types in your comment, but listed as a simple string here
"episode",
"season",
"title",
"year",
"originaltitle",
"imdbid",
"tmdbid",
"anidbid",
"premiered",
"genre",
"releasedate",
"plot",
"director",
"rating",
"runtime",
"studio",
"art",
"actor",
"showtitle",
"aired",
"customrating",
"dateadded",
"sorttitle",
"mpaa",
"aspectratio",
"collectionnumber",
"criticrating",
];
@@ -0,0 +1 @@
create a lib for that
@@ -0,0 +1,26 @@
export * from './MetaDataAPI';
export * from './MetaDataName';
export * from './metadata-type/basic/FileRef';
export * from './metadata-type/basic/LanguageData';
export * from './metadata-type/basic/RecordSet';
export * from './metadata-type/basic/SimpleFileType';
export * from './metadata-type/basic/StringBoolean';
export * from './metadata-type/basic/VideoType';
export * from "./metadata-type/basic/ArrayKey";
export * from './metadata-type/AnimeMeta';
export * from './metadata-type/DebugMeta';
export * from './metadata-type/FileNameMeta';
export * from './metadata-type/FileStatMeta';
export * from './metadata-type/GenreMeta';
export * from './metadata-type/HashMeta';
export * from './metadata-type/JellyfinMeta';
export * from './metadata-type/LanguagesMeta';
export * from './metadata-type/MediaStreamMeta';
export * from './metadata-type/SubtitleMeta';
export * from './metadata-type/TagsMeta';
export * from './metadata-type/TorrentMeta';
export * from './metadata-type/VideoMeta';
export * from './metadata-type/VersionMeta';
@@ -0,0 +1,9 @@
import {StringBoolean} from "./basic/StringBoolean.js";
export interface AnimeMeta {
anime?: StringBoolean;
}
export const AnimeMetaFields = [
"anime",
];
@@ -0,0 +1,7 @@
export interface DebugMeta {
privateFilePath?: string;
}
export const DebugMetaFields = [
"privateFilePath",
];
@@ -0,0 +1,9 @@
export interface FileNameMeta {
fileName?: string;
extension?: string;
}
export const FileNameMetaFields = [
"fileName",
"extension",
];
@@ -0,0 +1,12 @@
export interface FileStatMeta {
fileType?:string;
mimeType?:string;
sizeByte?:string;
}
export const FileStatMetaFields = [
"fileType",
"mimeType",
"sizeByte",
];
@@ -0,0 +1,9 @@
import {RecordSet} from "./basic/RecordSet.js";
export interface GenreMeta{
genres?: RecordSet;
}
export const GenreMetaFields = [
"genres",
];
@@ -0,0 +1,32 @@
/**
* HashMeta - Content identification hashes
*
* Primary Global ID: cid_midhash256 (SHA-256 hash of middle 1MB + file size)
* - Format: CID v1 with custom multicodec 0x1000
* - Example: bafkr4ih5kapbjzqvmj7jxr... (base32 CID encoding)
*
* Legacy IDs: cid_sha2-256, cid_sha1 (computed optionally for compatibility)
*
* Migration Note: cid_midhash256 is the new primary identifier replacing full SHA-256.
* Old hashes remain for backward compatibility and external system integration.
* All CIDs now use proper CID v1 format (IPFS-compatible structure).
*/
export interface HashMeta {
cid_crc32?: string; //0x0132 CRC32 (SFV)
cid_md5?: string; // 0xd5 md5
cid_sha1?: string; // 0x11 sha1
"cid_sha2-256"?: string;//0x12 sha2-256
"cid_sha3-256"?: string; // 0x16 sha3-256
"cid_sha3_384"?: string; // 0x16 sha3-256
cid_midhash256?: string; // 0x1000 midhash256 (CID v1 format with custom multicodec)
}
export const HashMetaFields = [
"cid_crc32",
"cid_md5",
"cid_sha1",
"cid_sha2-256",
"cid_sha3-256",
"cid_sha3_384",
"cid_midhash256",
];
@@ -0,0 +1,64 @@
import {LanguageData} from "./basic/LanguageData.js";
import {RecordSet} from "./basic/RecordSet.js";
import {VideoMeta} from "./VideoMeta.js";
import {TagsMeta} from "./TagsMeta.js";
import {LanguagesMeta} from "./LanguagesMeta.js";
import {GenreMeta} from "./GenreMeta.js";
import {FileRef} from "./basic/FileRef";
export interface IMDBMeta{
imdbid?: string;
criticrating?: string;
}
export interface TMDBMeta{
tmdbid?: string;
}
export interface AnidbMeta{
anidbid?: string;
}
export interface JellyfinMeta extends
IMDBMeta,
TMDBMeta,
AnidbMeta,
VideoMeta,
TagsMeta,
LanguagesMeta,
GenreMeta
{
releasedate?: string;
plot?: LanguageData<string>;//key is language ISO 639-1 code
rating?: string;
studio?: RecordSet;
art?:{
poster?: FileRef;
fanart?: FileRef;
};
mpaa?: string;
//TODO add more fields in the future
}
export const JellyfinMetaFields = [
"imdbid",
"tmdbid",
"anidbid",
"extra",
"season",
"episode",
"increment",
"movieYear",
"videoType",
"originalTitle",
"titles",
"tags",
"languages",
"genre",
"releasedate",
"plot",
"rating",
"studio",
"art",
"mpaa",
];
@@ -0,0 +1,10 @@
import {TypedRecordSet} from "./basic/RecordSet.js";
import {ISO639_1_CODE} from "./basic/LanguageData.js";
export interface LanguagesMeta{
languages?: TypedRecordSet<ISO639_1_CODE>;
}
export const LanguagesMetaFields = [
"languages",
];
@@ -0,0 +1,59 @@
import {StringBoolean} from "./basic/StringBoolean.js";
import {ArrayKey} from "./basic/ArrayKey";
export interface VideoStream {
width?: string;
height?: string;
bitrate?: string;
frameRate?: string;
rotation?: string;
language?: string;
}
export interface SubtitleStream {
language?: string;
title?: string;
}
export interface AudioStream {
sampleRate?: string;
bitrate?: string;
language?: string;
channelLayout?: string;
}
export interface CommonStream {
index?: string;
duration?: string;
codecType?: string;
codec?: string;
language?: string;
title?: string;
default?: StringBoolean;
forced?: StringBoolean;
}
export type Stream = VideoStream & SubtitleStream & AudioStream & CommonStream;
/**
* StreamList is a list of streams
* key is stream index in string "0","1","2" etc
*/
export type StreamList = Record<ArrayKey,Stream>;
export interface MediaStreamMeta {
fileinfo?: {
duration?: string;
formatName?: string;
streamdetails?: {
video: StreamList;
audio: StreamList;
subtitle: StreamList;
embeddedimage: StreamList;
}
}
}
export const MediaStreamMetaFields = [
"fileinfo",
];
@@ -0,0 +1,23 @@
import {LanguageData} from "./basic/LanguageData.js";
import {TypedRecordSet} from "./basic/RecordSet.js";
import {FileRef} from "./basic/FileRef.js";
//used by video files to store the subtitle files
export interface SubtitleMeta {
subtitles?:LanguageData<TypedRecordSet<FileRef>>;//key is the language code, value is a set of ipfs hash of the subtitle file
}
//used by subtitle files to store the video files
export interface SubtitleFileMeta {
videos?:TypedRecordSet<FileRef>;//a set of ipfs hash of the video file
subtitleLanguage?:string;//the language of the subtitle file
}
export const SubtitleMetaFields = [
"subtitles",
];
export const SubtitleFileMetaFields = [
"videos",
"subtitleLanguage",
];
@@ -0,0 +1,9 @@
import {RecordSet} from "./basic/RecordSet.js";
export interface TagsMeta {
tags?: RecordSet;
}
export const TagsMetaFields = [
"tags",
];
@@ -0,0 +1,25 @@
import {RecordSet} from "./basic/RecordSet.js";
export interface TorrentMeta {
announce?: string;
announceList?: RecordSet;
comment?: string;
createdBy?: string;
creationDate?: number;
info: {
files: Record<string, {
length: number;
path: string;
}>;// key is the "0" "1" "2" etc
name: string;
};
}
export const TorrentMetaFields = [
"announce",
"announceList",
"comment",
"createdBy",
"creationDate",
"info",
];
@@ -0,0 +1,7 @@
export interface VersionMeta {
version: string; //usually a V1 V2 V3 etc..
}
export const VersionMetaFields = [
"version"
];
@@ -0,0 +1,24 @@
import {LanguageData} from "./basic/LanguageData.js";
import {VideoType} from "./basic/VideoType";
export interface VideoMeta {
extra?: string;
season?: string;
episode?: string;
increment?: string;
movieYear?: string;
videoType?: VideoType;
originalTitle?: string;//title in original language
titles?: LanguageData<string>;// <ISO 639-3 lang codes, title>
}
export const VideoMetaFields = [
"extra",
"season",
"episode",
"increment",
"movieYear",
"videoType",
"originalTitle",
"titles",
];
@@ -0,0 +1,6 @@
// used to simulate array using map and object
export type ArrayKey = "id_0" | "id_1" | "id_2" | "id_3" | "id_4" | "id_5" | "id_6" | "id_7" | "id_8" | "id_9";
export function createArrayKey (index: number): ArrayKey {
return `id_${index}` as ArrayKey;
}
@@ -0,0 +1,4 @@
export type FileRef = URLString | FilePathString | CIDString;
export type URLString = string;
export type FilePathString = string;
export type CIDString = string;
@@ -0,0 +1,12 @@
/**
* key is language ISO 639-3 code
* https://en.wikipedia.org/wiki/List_of_ISO_639-3_codes
*/
export type LanguageData<T> = Record<string, T>;
export type ISO639_1_CODE = string;
/**
* jpn, jpr and jpa already exist. Creating jpl is for japan-latin. Used for japanese romaji NON ISO 639-3 standard.
* */
export const romajiIsoCode = "jpl";
export const englishIsoCode = "eng";
@@ -0,0 +1,2 @@
export type RecordSet = Set<string>;
export type TypedRecordSet<T extends string> = Set<T>;
@@ -0,0 +1,2 @@
export type SimpleFileType = 'audio' | 'video' | 'document' | 'archive' | 'other' | 'undefined';
@@ -0,0 +1 @@
export type StringBoolean = "true" | "false";
@@ -0,0 +1 @@
export type VideoType = "movie" | "tvshow";
@@ -0,0 +1,14 @@
<!-- Example XSD: mediaFileSchema.xsd -->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.example.com/schema" xmlns="http://www.example.com/schema" elementFormDefault="qualified">
<xs:element name="root">
<xs:complexType>
<xs:sequence>
<xs:element name="fileType" type="xs:string"/>
<xs:element name="mimeType" type="xs:string"/>
<xs:element name="byteSize" type="xs:long"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>