initial commit
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user