diff --git a/files/file.go b/files/file.go
index e0fdb162..164f9fc5 100644
--- a/files/file.go
+++ b/files/file.go
@@ -14,6 +14,7 @@ import (
"os"
"path"
"path/filepath"
+ "regexp"
"strings"
"time"
@@ -307,8 +308,23 @@ func (i *FileInfo) detectSubtitles() {
}
base := strings.TrimSuffix(i.Name, ext)
+ subDirsRegex := regexp.MustCompile("(?i)^sub(s|titles)$")
for _, f := range dir {
- if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
+ // load all .vtt/.srt subtitles from subs directories
+ if f.IsDir() && subDirsRegex.MatchString(f.Name()) {
+ subsDir := path.Join(parentDir, f.Name())
+ var err error
+ dir, err = afero.ReadDir(i.Fs, subsDir)
+ if err == nil {
+ for _, f := range dir {
+ if !f.IsDir() && (strings.HasSuffix(f.Name(), ".vtt") ||
+ strings.HasSuffix(f.Name(), ".srt")) {
+ i.Subtitles = append(i.Subtitles, path.Join(subsDir, f.Name()))
+ }
+ }
+ }
+ } else if !f.IsDir() && strings.HasPrefix(f.Name(), base) &&
+ (strings.HasSuffix(f.Name(), ".vtt") || strings.HasSuffix(f.Name(), ".srt")) {
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
}
}
diff --git a/frontend/src/components/files/VideoPlayer.vue b/frontend/src/components/files/VideoPlayer.vue
index e1b18a27..4f96e993 100644
--- a/frontend/src/components/files/VideoPlayer.vue
+++ b/frontend/src/components/files/VideoPlayer.vue
@@ -6,14 +6,6 @@
style="width: 100%; height: 100%"
>
Sorry, your browser doesn't support embedded videos, but don't worry, you
can download it
@@ -22,64 +14,81 @@
-
diff --git a/frontend/src/utils/subtitle.ts b/frontend/src/utils/subtitle.ts
new file mode 100644
index 00000000..dfeaeca3
--- /dev/null
+++ b/frontend/src/utils/subtitle.ts
@@ -0,0 +1,72 @@
+import { StatusError } from "@/api/utils";
+
+export async function loadSubtitle(subUrl: string) {
+ let url: URL;
+ try {
+ url = new URL(subUrl);
+ } catch (_) {
+ // treat it as a relative url
+ // we only need this for filename
+ url = new URL(subUrl, window.location.origin);
+ }
+
+ const label = decodeURIComponent(
+ url.pathname
+ .split("/")
+ .pop()!
+ .replace(/\.[^/.]+$/, "")
+ );
+ let src;
+ if (url.pathname.toLowerCase().endsWith(".srt")) {
+ try {
+ const resp = await fetch(subUrl);
+ if (!resp.ok) {
+ throw new StatusError(
+ `Failed to fetch subtitle from ${subUrl}!`,
+ resp.status
+ );
+ }
+ const vtt = srtToVttBlob(await resp.text());
+ src = URL.createObjectURL(vtt);
+ } catch (error) {
+ console.error(error);
+ }
+ } else if (url.pathname.toLowerCase().endsWith(".vtt")) {
+ src = subUrl;
+ }
+ return { src, label };
+}
+
+export function srtToVttBlob(srtData: string) {
+ const VTT_HEAD = "WEBVTT\n\n";
+ // Replace line breaks with \n
+ let subtitles = srtData.replace(/\r\n|\r|\n/g, "\n");
+ // commas -> dots in timestamps
+ subtitles = subtitles.replace(/(\d\d:\d\d:\d\d),(\d\d\d)/g, "$1.$2");
+ // map SRT font colors to VTT cue span classes
+ const colorMap: Record
/g, "\n");
+ let vttSubtitles = VTT_HEAD;
+ if (Object.keys(colorMap).length) {
+ let vttStyles = "";
+ for (const cssClass in colorMap) {
+ const color = colorMap[cssClass];
+ // add cue style declaration
+ vttStyles += `::cue(.${cssClass}) {color: ${color};}\n`;
+ }
+ vttSubtitles += vttStyles;
+ }
+ vttSubtitles += "\n"; // an empty line MUST separate styles from subs
+ vttSubtitles += subtitles;
+ return new Blob([vttSubtitles], { type: "text/vtt" });
+}