From 2ee313d5f663163c547a08dfab0b2a997d1cef9a Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Fri, 3 Jan 2025 21:26:51 -0300 Subject: [PATCH] Add earing Impaired suptitle flag support (#754) --- .../models/src/resources/watch-info.ts | 4 +++ .../packages/ui/src/components/media-info.tsx | 3 +++ .../src/player/components/right-buttons.tsx | 6 ++--- front/packages/ui/src/utils.ts | 26 +++++++++++++++--- front/translations/en.json | 4 ++- front/translations/pt_br.json | 4 ++- ...00002_add_hearing_impaired_column.down.sql | 5 ++++ .../000002_add_hearing_impaired_column.up.sql | 5 ++++ transcoder/src/info.go | 19 +++++++------ transcoder/src/metadata.go | 13 ++++----- transcoder/src/subtitles.go | 27 ++++++++++++++++--- transcoder/src/utils.go | 11 ++++++++ 12 files changed, 101 insertions(+), 26 deletions(-) create mode 100644 transcoder/migrations/000002_add_hearing_impaired_column.down.sql create mode 100644 transcoder/migrations/000002_add_hearing_impaired_column.up.sql diff --git a/front/packages/models/src/resources/watch-info.ts b/front/packages/models/src/resources/watch-info.ts index 134da3432..4363aa0fc 100644 --- a/front/packages/models/src/resources/watch-info.ts +++ b/front/packages/models/src/resources/watch-info.ts @@ -95,6 +95,10 @@ export const SubtitleP = TrackP.extend({ * Is this an external subtitle (as in stored in a different file) */ isExternal: z.boolean(), + /** + * Is this a hearing impaired subtitle? + */ + isHearingImpaired: z.boolean(), }); export type Subtitle = z.infer; diff --git a/front/packages/ui/src/components/media-info.tsx b/front/packages/ui/src/components/media-info.tsx index 8ff05f16c..0ed2416e0 100644 --- a/front/packages/ui/src/components/media-info.tsx +++ b/front/packages/ui/src/components/media-info.tsx @@ -59,6 +59,9 @@ const MediaInfoTable = ({ // Only show it if there is more than one track track.isDefault && !singleTrack ? t("mediainfo.default") : undefined, track.isForced ? t("mediainfo.forced") : undefined, + "isHearingImpaired" in track && track.isHearingImpaired + ? t("mediainfo.hearing-impaired") + : undefined, "isExternal" in track && track.isExternal ? t("mediainfo.external") : undefined, track.codec, ] diff --git a/front/packages/ui/src/player/components/right-buttons.tsx b/front/packages/ui/src/player/components/right-buttons.tsx index b11131009..57c17fa5c 100644 --- a/front/packages/ui/src/player/components/right-buttons.tsx +++ b/front/packages/ui/src/player/components/right-buttons.tsx @@ -29,7 +29,7 @@ import { useAtom } from "jotai"; import { useTranslation } from "react-i18next"; import { Platform, View } from "react-native"; import { type Stylable, useYoshiki } from "yoshiki/native"; -import { useDisplayName } from "../../utils"; +import { useSubtitleName } from "../../utils"; import { fullscreenAtom, subtitleAtom } from "../state"; import { AudiosMenu, QualitiesMenu } from "../video"; @@ -49,7 +49,7 @@ export const RightButtons = ({ } & Stylable) => { const { css } = useYoshiki(); const { t } = useTranslation(); - const getDisplayName = useDisplayName(); + const getSubtitleName = useSubtitleName(); const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom); const [selectedSubtitle, setSubtitle] = useAtom(subtitleAtom); @@ -74,7 +74,7 @@ export const RightButtons = ({ {subtitles.map((x, i) => ( setSubtitle(x)} diff --git a/front/packages/ui/src/utils.ts b/front/packages/ui/src/utils.ts index 18ca5f57d..b46a67fce 100644 --- a/front/packages/ui/src/utils.ts +++ b/front/packages/ui/src/utils.ts @@ -1,5 +1,7 @@ -import type { Track } from "@kyoo/models"; +import type { Subtitle, Track } from "@kyoo/models"; + import intl from "langmap"; +import { useTranslation } from "react-i18next"; export const useLanguageName = () => { return (lang: string) => intl[lang]?.nativeName; @@ -7,6 +9,7 @@ export const useLanguageName = () => { export const useDisplayName = () => { const getLanguageName = useLanguageName(); + const { t } = useTranslation(); return (sub: Track) => { const lng = sub.language ? getLanguageName(sub.language) : null; @@ -14,8 +17,25 @@ export const useDisplayName = () => { if (lng && sub.title && sub.title !== lng) return `${lng} - ${sub.title}`; if (lng) return lng; if (sub.title) return sub.title; - if (sub.index !== null) return `Unknown (${sub.index})`; - return "Unknown"; + if (sub.index !== null) return `${t("mediainfo.unknown")} (${sub.index})`; + return t("mediainfo.unknown"); + }; +}; + +export const useSubtitleName = () => { + const getDisplayName = useDisplayName(); + const { t } = useTranslation(); + + return (sub: Subtitle) => { + const name = getDisplayName(sub); + const attributes = [name]; + + if (sub.isDefault) attributes.push(t("mediainfo.default")); + if (sub.isForced) attributes.push(t("mediainfo.forced")); + if (sub.isHearingImpaired) attributes.push(t("mediainfo.hearing-impaired")); + if (sub.isExternal) attributes.push(t("mediainfo.external")); + + return attributes.join(" - "); }; }; diff --git a/front/translations/en.json b/front/translations/en.json index 949b53464..4e3609a27 100644 --- a/front/translations/en.json +++ b/front/translations/en.json @@ -259,12 +259,14 @@ "audio": "Audio", "subtitles": "Subtitles", "forced": "Forced", + "hearing-impaired": "CC", "default": "Default", "external": "External", "duration": "Duration", "size": "Size", "novideo": "No video", - "nocontainer": "Invalid container" + "nocontainer": "Invalid container", + "unknown": "Unknown" }, "admin": { "users": { diff --git a/front/translations/pt_br.json b/front/translations/pt_br.json index ae6493002..b55293bfe 100644 --- a/front/translations/pt_br.json +++ b/front/translations/pt_br.json @@ -259,12 +259,14 @@ "audio": "Áudio", "subtitles": "Legendas", "forced": "Forçado", + "hearing-impaired": "CC", "default": "Padrão", "duration": "Duração", "size": "Tamanho", "novideo": "Sem vídeo", "nocontainer": "Contêiner inválido", - "external": "Externo" + "external": "Externo", + "unknown": "Desconhecido" }, "admin": { "users": { diff --git a/transcoder/migrations/000002_add_hearing_impaired_column.down.sql b/transcoder/migrations/000002_add_hearing_impaired_column.down.sql new file mode 100644 index 000000000..9b5e0058d --- /dev/null +++ b/transcoder/migrations/000002_add_hearing_impaired_column.down.sql @@ -0,0 +1,5 @@ +begin; + +alter table subtitles drop column is_hearing_impaired; + +commit; diff --git a/transcoder/migrations/000002_add_hearing_impaired_column.up.sql b/transcoder/migrations/000002_add_hearing_impaired_column.up.sql new file mode 100644 index 000000000..36192861d --- /dev/null +++ b/transcoder/migrations/000002_add_hearing_impaired_column.up.sql @@ -0,0 +1,5 @@ +begin; + +alter table subtitles add column is_hearing_impaired boolean not null default false; + +commit; diff --git a/transcoder/src/info.go b/transcoder/src/info.go index a1e167fb1..f480c1603 100644 --- a/transcoder/src/info.go +++ b/transcoder/src/info.go @@ -123,6 +123,8 @@ type Subtitle struct { IsDefault bool `json:"isDefault"` /// Is this stream tagged as forced? IsForced bool `json:"isForced"` + /// Is this stream tagged as hearing impaired? + IsHearingImpaired bool `json:"isHearingImpaired"` /// Is this an external subtitle (as in stored in a different file) IsExternal bool `json:"isExternal"` /// Where the subtitle is stored (null if stored inside the video) @@ -287,14 +289,15 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) { lang, _ := language.Parse(stream.Tags.Language) idx := uint32(i) return Subtitle{ - Index: &idx, - Title: OrNull(stream.Tags.Title), - Language: NullIfUnd(lang.String()), - Codec: stream.CodecName, - Extension: extension, - IsDefault: stream.Disposition.Default != 0, - IsForced: stream.Disposition.Forced != 0, - Link: &link, + Index: &idx, + Title: OrNull(stream.Tags.Title), + Language: NullIfUnd(lang.String()), + Codec: stream.CodecName, + Extension: extension, + IsDefault: stream.Disposition.Default != 0, + IsForced: stream.Disposition.Forced != 0, + IsHearingImpaired: stream.Disposition.HearingImpaired != 0, + Link: &link, } }), Chapters: Map(mi.Chapters, func(c *ffprobe.Chapter, _ int) Chapter { diff --git a/transcoder/src/metadata.go b/transcoder/src/metadata.go index 259d6b57c..155c68639 100644 --- a/transcoder/src/metadata.go +++ b/transcoder/src/metadata.go @@ -162,7 +162,7 @@ func (s *MetadataService) getMetadata(path string, sha string) (*MediaInfo, erro } rows, err = s.database.Query( - `select s.idx, s.title, s.language, s.codec, s.extension, s.is_default, s.is_forced + `select s.idx, s.title, s.language, s.codec, s.extension, s.is_default, s.is_forced, s.is_hearing_impaired from subtitles as s where s.sha=$1`, sha, ) @@ -171,7 +171,7 @@ func (s *MetadataService) getMetadata(path string, sha string) (*MediaInfo, erro } for rows.Next() { var s Subtitle - err := rows.Scan(&s.Index, &s.Title, &s.Language, &s.Codec, &s.Extension, &s.IsDefault, &s.IsForced) + err := rows.Scan(&s.Index, &s.Title, &s.Language, &s.Codec, &s.Extension, &s.IsDefault, &s.IsForced, &s.IsHearingImpaired) if err != nil { return nil, err } @@ -273,8 +273,8 @@ func (s *MetadataService) storeFreshMetadata(path string, sha string) (*MediaInf } for _, s := range ret.Subtitles { tx.Exec(` - insert into subtitles(sha, idx, title, language, codec, extension, is_default, is_forced) - values ($1, $2, $3, $4, $5, $6, $7, $8) + insert into subtitles(sha, idx, title, language, codec, extension, is_default, is_forced, is_hearing_impaired) + values ($1, $2, $3, $4, $5, $6, $7, $8, $9) on conflict (sha, idx) do update set sha = excluded.sha, idx = excluded.idx, @@ -283,9 +283,10 @@ func (s *MetadataService) storeFreshMetadata(path string, sha string) (*MediaInf codec = excluded.codec, extension = excluded.extension, is_default = excluded.is_default, - is_forced = excluded.is_forced + is_forced = excluded.is_forced, + is_hearing_impaired = excluded.is_hearing_impaired `, - ret.Sha, s.Index, s.Title, s.Language, s.Codec, s.Extension, s.IsDefault, s.IsForced, + ret.Sha, s.Index, s.Title, s.Language, s.Codec, s.Extension, s.IsDefault, s.IsForced, s.IsHearingImpaired, ) } for _, c := range ret.Chapters { diff --git a/transcoder/src/subtitles.go b/transcoder/src/subtitles.go index 64ff95497..115692998 100644 --- a/transcoder/src/subtitles.go +++ b/transcoder/src/subtitles.go @@ -43,26 +43,45 @@ outer: Path: &match, Link: &link, } - flags := separator.Split(match[len(base_path):], -1) + flags_str := strings.ToLower(match[len(base_path):]) + flags := separator.Split(flags_str, -1) + // remove extension from flags flags = flags[:len(flags)-1] for _, flag := range flags { - switch strings.ToLower(flag) { + switch flag { case "default": sub.IsDefault = true case "forced": sub.IsForced = true + case "hi", "sdh", "cc": + sub.IsHearingImpaired = true default: lang, err := language.Parse(flag) if err == nil && lang != language.Und { - lang := lang.String() - sub.Language = &lang + langStr := lang.String() + sub.Language = &langStr } else { sub.Title = &flag } } } + + // Handle Hindi (hi) collision with Hearing Impaired (hi): + // "hi" by itself means a language code, but when combined with other lang flags it means Hearing Impaired. + // In case Hindi was not detected before, but "hi" is present, assume it is Hindi. + if sub.Language == nil { + hiCount := Count(flags, "hi") + if hiCount > 0 { + languageStr := language.Hindi.String() + sub.Language = &languageStr + } + if hiCount == 1 { + sub.IsHearingImpaired = false + } + } + mi.Subtitles = append(mi.Subtitles, sub) continue outer } diff --git a/transcoder/src/utils.go b/transcoder/src/utils.go index 577c4f6ba..2e9d47497 100644 --- a/transcoder/src/utils.go +++ b/transcoder/src/utils.go @@ -25,3 +25,14 @@ func Filter[E any](s []E, f func(E) bool) []E { } return s2 } + +// Count returns the number of elements in s that are equal to e. +func Count[S []E, E comparable](s S, e E) int { + var n int + for _, v := range s { + if v == e { + n++ + } + } + return n +}