Skip to content

Commit

Permalink
fix(transcript): fix not respond to local subtitle deletion
Browse files Browse the repository at this point in the history
  • Loading branch information
aidenlx committed May 8, 2024
1 parent 5ffa6de commit 147074f
Show file tree
Hide file tree
Showing 19 changed files with 344 additions and 177 deletions.
12 changes: 9 additions & 3 deletions apps/app/src/components/use-tracks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ function toWebpageID(wid: string) {
return webpageTrackPrefix + wid;
}
function toWebpageUrl(wid: string) {
return `webview://${toWebpageID(wid)}}`;
return `webview://${toWebpageID(wid)}`;
}
function getWebpageIDFromTrackID(id: string) {
if (!id.startsWith(webpageTrackPrefix)) return null;
Expand All @@ -187,13 +187,19 @@ export function dedupeWebsiteTrack(
return website.filter((t) => !local.some(({ wid }) => wid === t.wid));
}

export function toTrackLabel(t: TextTrack, idx: number) {
interface SortableTrack {
language?: string;
label?: string;
kind: string;
}

export function toTrackLabel(t: SortableTrack, idx: number) {
return (
t.label || langCodeToLabel(t.language) || `${upperFirst(t.kind)} ${idx + 1}`
);
}

export function sortTrack(a: TextTrack | null, b: TextTrack | null) {
export function sortTrack(a: SortableTrack | null, b: SortableTrack | null) {
if (a && b) {
// if with item.track.language, sort by item.track.language
if (a.language && b.language) {
Expand Down
33 changes: 23 additions & 10 deletions apps/app/src/info/track-info.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { fileURLToPath } from "url";
import type { ParsedCaptionsResult } from "media-captions";
import { TFile } from "obsidian";
import { toFileInfo, type FileInfo } from "@/lib/iter-sibling";
import { toFileInfo, type FileInfo } from "@/lib/file-info";
import { format } from "@/lib/lang/lang";
import { noHash, toURL } from "@/lib/url";

const allowedProtocols = new Set(["https:", "http:", "file:"]);

type TextTrackKind = "captions" | "subtitles";
export type TextTrackKind = "captions" | "subtitles";
export const textTrackFmField = {
subtitles: {
singular: "subtitle",
plural: "subtitles",
},
captions: {
singular: "caption",
plural: "captions",
},
} as const;
export const textTrackKinds = Object.keys(
textTrackFmField,
) as (keyof typeof textTrackFmField)[];

/**
* for those extracted from web, provide a unique id from web provider
Expand Down Expand Up @@ -102,13 +115,9 @@ function parseURL(
let { language, type } = fromHash;

// parse as a file
const name = src.pathname.split("/").pop() ?? "";
const extension = name.split(".").pop() ?? "";
if (name && extension && isSupportedCaptionExt(extension)) {
const { language: lang } = parseTrackFromBasename(
// remove extension from filename
name.slice(0, -extension.length - 1),
);
const { basename, extension } = toFileInfo(src.pathname, "/");
if (isSupportedCaptionExt(extension)) {
const { language: lang } = parseTrackFromBasename(basename);
if (lang) language ??= lang;
type ??= extension;
return { wid, language, label, type, kind, src };
Expand Down Expand Up @@ -160,7 +169,7 @@ export function toTrack<F extends FileInfo>(
subpath = "",
alias,
}: Partial<{
kind: "captions" | "subtitles";
kind: TextTrackKind;
subpath: string;
alias: string;
}> = {},
Expand All @@ -185,3 +194,7 @@ export function getTrackInfoID({ wid, src }: TextTrackInfo) {
const srcURL = src instanceof URL ? noHash(src) : src.path;
return { id: `${type}:${srcURL}`, wid };
}

export function isVaultTrack(trackID: string) {
return trackID.startsWith("file:");
}
30 changes: 30 additions & 0 deletions apps/app/src/lib/file-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export interface FileInfo {
extension: string;
basename: string;
path: string;
}

export function toFileInfo(
filepath: string,
sep = "/",
): FileInfo & { name: string; parent: string } {
const filename = filepath.split(sep).pop()!;
const extension = filename.split(".").pop()!;
const info = {
name: filename,
path: filepath,
parent: filepath.slice(0, -filename.length - 1),
};
if (extension === filename) {
return {
extension: "",
basename: filename,
...info,
};
}
return {
extension,
basename: filename.slice(0, -extension.length - 1),
...info,
};
}
27 changes: 3 additions & 24 deletions apps/app/src/lib/iter-sibling.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,7 @@
import path from "@/lib/path";
import { getFsPromise } from "@/web/session/utils";

export interface FileInfo {
extension: string;
basename: string;
path: string;
}

export function toFileInfo(filepath: string): FileInfo {
const name = path.basename(filepath);
const segs = name.split(".");
if (segs.length === 1)
return {
extension: "",
basename: name,
path: filepath,
};

return {
extension: segs.at(-1)!,
basename: segs.slice(0, -1).join("."),
path: filepath,
};
}
import type { FileInfo } from "./file-info";
import { toFileInfo } from "./file-info";

/**
* @param exclude names of files under the directory to exclude
Expand All @@ -37,6 +16,6 @@ export async function* iterSiblings(
const dir = await fs.opendir(dirPath, { encoding: "utf-8" });
for await (const f of dir) {
if (!(f.isFile() || f.isSymbolicLink()) || excludeSet.has(f.name)) continue;
yield toFileInfo(path.join(dirPath, f.name));
yield toFileInfo(path.join(dirPath, f.name), path.sep);
}
}
2 changes: 1 addition & 1 deletion apps/app/src/media-note/note-index/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function getMediaNoteMeta(
return {
src,
get data() {
return parseMediaNoteMeta(meta, ctx);
return parseMediaNoteMeta(meta);
},
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import type { MediaPlayerInstance } from "@vidstack/react";
import { Component, debounce } from "obsidian";
import type { MetadataCache, Vault, TFile } from "obsidian";
import { Component, debounce } from "obsidian";
import { getMediaInfoID, isFileMediaInfo } from "@/info/media-info";
import { type MediaInfo } from "@/info/media-info";
import { checkMediaType } from "@/info/media-type";
import { getTrackInfoID, type TextTrackInfo } from "@/info/track-info";
import type { TextTrackInfo } from "@/info/track-info";
import { iterateFiles } from "@/lib/iterate-files";
import { waitUntilResolve } from "@/lib/meta-resolve";
import { waitUntilResolve as waitUntilMetaInited } from "@/lib/meta-resolve";
import { normalizeFilename } from "@/lib/norm";
import type { PlayerComponent } from "@/media-view/base";
import type MxPlugin from "@/mx-main";
import { TranscriptIndex } from "../../transcript/handle/indexer";
import { mediaTitle } from "../title";
import type { MediaSourceFieldType } from "./def";
import type { ParsedMediaNoteMetadata } from "./extract";
Expand All @@ -25,38 +26,31 @@ declare module "obsidian" {
interface MetadataCache {
on(name: "finished", callback: () => any, ctx?: any): EventRef;
on(name: "initialized", callback: () => any, ctx?: any): EventRef;
on(
name: "mx:transcript-changed",
callback: (trackIDs: Set<string>, mediaID: string) => any,
ctx?: any,
): EventRef;
trigger(
name: "mx:transcript-changed",
trackIDs: Set<string>,
mediaID: string,
): void;
initialized: boolean;
}
}

export class MediaNoteIndex extends Component {
app;
constructor(public plugin: MxPlugin) {
super();
this.app = plugin.app;
this.transcript = this.addChild(new TranscriptIndex(this.plugin));
}

// media - note map is one-to-one
private noteToMediaIndex = new Map<string, MediaInfo>();
private mediaToNoteIndex = new Map<string, TFile>();

private mediaToTrackIndex = new Map<string, TextTrackInfo[]>();
private trackToMediaIndex = new Map<string, MediaInfo[]>();
private transcript;

getLinkedTextTracks(media: MediaInfo) {
return this.mediaToTrackIndex.get(getMediaInfoID(media)) ?? [];
const note = this.findNote(media);
if (!note) return [];
return this.transcript.getLinkedTextTracks(note);
}
getLinkedMedia(track: TextTrackInfo) {
return this.trackToMediaIndex.get(getTrackInfoID(track).id) ?? [];
getLinkedMedia(track: TextTrackInfo): MediaInfo[] {
const notes = this.transcript.getLinkedMediaNotes(track);
return notes.map((note) => this.findMedia(note)!);
}

findNote(media: MediaInfo): TFile | null {
Expand All @@ -66,9 +60,10 @@ export class MediaNoteIndex extends Component {
return this.noteToMediaIndex.get(note.path);
}

private onResolve() {
private onResolved() {
this.noteToMediaIndex.clear();
this.mediaToNoteIndex.clear();
this.transcript.clear();
const ctx = {
metadataCache: this.app.metadataCache,
vault: this.app.vault,
Expand All @@ -95,10 +90,27 @@ export class MediaNoteIndex extends Component {
const mediaInfo = this.noteToMediaIndex.get(oldPath)!;
this.noteToMediaIndex.delete(oldPath);
this.noteToMediaIndex.set(file.path, mediaInfo);
// mediaToNoteIndex don't need to update
// media(track)ToNoteIndex don't need to update
// since TFile pointer is not changed
}),
);
this.register(
this.transcript.on("changed", (updated, note) => {
const mediaInfo = this.findMedia(note);
if (!mediaInfo) {
console.warn(
"Media not found for note while responding to transcript change",
note.path,
);
return;
}
this.plugin.app.metadataCache.trigger(
"mx:transcript-changed",
updated,
getMediaInfoID(mediaInfo),
);
}),
);
}

/**
Expand Down Expand Up @@ -167,41 +179,13 @@ export class MediaNoteIndex extends Component {
return newNote;
}

private resetTracks(mediaID: string): string[] {
const tracks = this.mediaToTrackIndex.get(mediaID);
if (!tracks) return [];
const affected = tracks.map((track) => getTrackInfoID(track).id);
this.mediaToTrackIndex.delete(mediaID);
tracks.forEach((track) => {
const trackID = getTrackInfoID(track).id;
const linkedMedia = this.trackToMediaIndex.get(trackID);
if (!linkedMedia) return;
const filteredMedia = linkedMedia.filter(
(media) => getMediaInfoID(media) !== mediaID,
);
if (filteredMedia.length > 0) {
this.trackToMediaIndex.set(trackID, filteredMedia);
} else {
this.trackToMediaIndex.delete(trackID);
}
});
return affected;
}

removeMediaNote(toRemove: TFile) {
const mediaInfo = this.noteToMediaIndex.get(toRemove.path)!;
removeMediaNote(note: TFile) {
const mediaInfo = this.noteToMediaIndex.get(note.path)!;
if (!mediaInfo) return;
this.noteToMediaIndex.delete(toRemove.path);
this.noteToMediaIndex.delete(note.path);
const mediaID = getMediaInfoID(mediaInfo);
this.mediaToNoteIndex.delete(mediaID);
const affected = this.resetTracks(mediaID);
if (affected.length > 0) {
this.app.metadataCache.trigger(
"mx:transcript-changed",
new Set(affected),
mediaID,
);
}
this.transcript.remove(note);
}
addMediaNote(meta: ParsedMediaNoteMetadata, newNote: TFile) {
const mediaID = getMediaInfoID(meta.src);
Expand All @@ -215,32 +199,12 @@ export class MediaNoteIndex extends Component {
return;
this.noteToMediaIndex.set(newNote.path, meta.src);
this.mediaToNoteIndex.set(mediaID, newNote);
const affected = new Set(this.resetTracks(mediaID));
const { textTracks } = meta.data;
if (textTracks.length > 0) {
this.mediaToTrackIndex.set(mediaID, textTracks);
textTracks.forEach((track) => {
const trackID = getTrackInfoID(track).id;
affected.add(trackID);
const linkedMedia = [
meta.src,
...(this.trackToMediaIndex.get(trackID) ?? []),
];
this.trackToMediaIndex.set(trackID, linkedMedia);
});
}
if (affected.size > 0) {
this.app.metadataCache.trigger(
"mx:transcript-changed",
affected,
mediaID,
);
}
this.transcript.add(newNote, meta.data.textTracks);
}

onload(): void {
waitUntilResolve(this.app.metadataCache, this).then(() => {
this.onResolve();
waitUntilMetaInited(this.app.metadataCache, this).then(() => {
this.onResolved();
});
}
}
Expand Down
18 changes: 5 additions & 13 deletions apps/app/src/media-note/note-index/parse.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import type { CachedMetadata, MetadataCache } from "obsidian";
import type { TextTrackInfo } from "@/info/track-info";
import type { CachedMetadata } from "obsidian";
import type { MetaTextTrackInfo } from "@/transcript/handle/meta";
import { parseTextTrackFields } from "@/transcript/handle/meta";

export interface MediaNoteMeta {
textTracks: TextTrackInfo[];
textTracks: MetaTextTrackInfo[];
}

export function parseMediaNoteMeta(
meta: CachedMetadata,
{
metadataCache,
sourcePath,
}: { metadataCache: MetadataCache; sourcePath: string },
): MediaNoteMeta {
export function parseMediaNoteMeta(meta: CachedMetadata): MediaNoteMeta {
return {
textTracks: parseTextTrackFields(meta, (linkpath) =>
metadataCache.getFirstLinkpathDest(linkpath, sourcePath),
),
textTracks: parseTextTrackFields(meta),
};
}

0 comments on commit 147074f

Please sign in to comment.