Add earing Impaired suptitle flag support (#754)

This commit is contained in:
Felipe Marinho 2025-01-03 21:26:51 -03:00 committed by GitHub
parent cd89e757c1
commit 2ee313d5f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 101 additions and 26 deletions

View File

@ -95,6 +95,10 @@ export const SubtitleP = TrackP.extend({
* Is this an external subtitle (as in stored in a different file) * Is this an external subtitle (as in stored in a different file)
*/ */
isExternal: z.boolean(), isExternal: z.boolean(),
/**
* Is this a hearing impaired subtitle?
*/
isHearingImpaired: z.boolean(),
}); });
export type Subtitle = z.infer<typeof SubtitleP>; export type Subtitle = z.infer<typeof SubtitleP>;

View File

@ -59,6 +59,9 @@ const MediaInfoTable = ({
// Only show it if there is more than one track // Only show it if there is more than one track
track.isDefault && !singleTrack ? t("mediainfo.default") : undefined, track.isDefault && !singleTrack ? t("mediainfo.default") : undefined,
track.isForced ? t("mediainfo.forced") : 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, "isExternal" in track && track.isExternal ? t("mediainfo.external") : undefined,
track.codec, track.codec,
] ]

View File

@ -29,7 +29,7 @@ import { useAtom } from "jotai";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { type Stylable, useYoshiki } from "yoshiki/native"; import { type Stylable, useYoshiki } from "yoshiki/native";
import { useDisplayName } from "../../utils"; import { useSubtitleName } from "../../utils";
import { fullscreenAtom, subtitleAtom } from "../state"; import { fullscreenAtom, subtitleAtom } from "../state";
import { AudiosMenu, QualitiesMenu } from "../video"; import { AudiosMenu, QualitiesMenu } from "../video";
@ -49,7 +49,7 @@ export const RightButtons = ({
} & Stylable) => { } & Stylable) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
const { t } = useTranslation(); const { t } = useTranslation();
const getDisplayName = useDisplayName(); const getSubtitleName = useSubtitleName();
const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom); const [isFullscreen, setFullscreen] = useAtom(fullscreenAtom);
const [selectedSubtitle, setSubtitle] = useAtom(subtitleAtom); const [selectedSubtitle, setSubtitle] = useAtom(subtitleAtom);
@ -74,7 +74,7 @@ export const RightButtons = ({
{subtitles.map((x, i) => ( {subtitles.map((x, i) => (
<Menu.Item <Menu.Item
key={x.index ?? i} key={x.index ?? i}
label={x.link ? getDisplayName(x) : `${getDisplayName(x)} (${x.codec})`} label={x.link ? getSubtitleName(x) : `${getSubtitleName(x)} (${x.codec})`}
selected={selectedSubtitle === x} selected={selectedSubtitle === x}
disabled={!x.link} disabled={!x.link}
onSelect={() => setSubtitle(x)} onSelect={() => setSubtitle(x)}

View File

@ -1,5 +1,7 @@
import type { Track } from "@kyoo/models"; import type { Subtitle, Track } from "@kyoo/models";
import intl from "langmap"; import intl from "langmap";
import { useTranslation } from "react-i18next";
export const useLanguageName = () => { export const useLanguageName = () => {
return (lang: string) => intl[lang]?.nativeName; return (lang: string) => intl[lang]?.nativeName;
@ -7,6 +9,7 @@ export const useLanguageName = () => {
export const useDisplayName = () => { export const useDisplayName = () => {
const getLanguageName = useLanguageName(); const getLanguageName = useLanguageName();
const { t } = useTranslation();
return (sub: Track) => { return (sub: Track) => {
const lng = sub.language ? getLanguageName(sub.language) : null; 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 && sub.title && sub.title !== lng) return `${lng} - ${sub.title}`;
if (lng) return lng; if (lng) return lng;
if (sub.title) return sub.title; if (sub.title) return sub.title;
if (sub.index !== null) return `Unknown (${sub.index})`; if (sub.index !== null) return `${t("mediainfo.unknown")} (${sub.index})`;
return "Unknown"; 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(" - ");
}; };
}; };

View File

@ -259,12 +259,14 @@
"audio": "Audio", "audio": "Audio",
"subtitles": "Subtitles", "subtitles": "Subtitles",
"forced": "Forced", "forced": "Forced",
"hearing-impaired": "CC",
"default": "Default", "default": "Default",
"external": "External", "external": "External",
"duration": "Duration", "duration": "Duration",
"size": "Size", "size": "Size",
"novideo": "No video", "novideo": "No video",
"nocontainer": "Invalid container" "nocontainer": "Invalid container",
"unknown": "Unknown"
}, },
"admin": { "admin": {
"users": { "users": {

View File

@ -259,12 +259,14 @@
"audio": "Áudio", "audio": "Áudio",
"subtitles": "Legendas", "subtitles": "Legendas",
"forced": "Forçado", "forced": "Forçado",
"hearing-impaired": "CC",
"default": "Padrão", "default": "Padrão",
"duration": "Duração", "duration": "Duração",
"size": "Tamanho", "size": "Tamanho",
"novideo": "Sem vídeo", "novideo": "Sem vídeo",
"nocontainer": "Contêiner inválido", "nocontainer": "Contêiner inválido",
"external": "Externo" "external": "Externo",
"unknown": "Desconhecido"
}, },
"admin": { "admin": {
"users": { "users": {

View File

@ -0,0 +1,5 @@
begin;
alter table subtitles drop column is_hearing_impaired;
commit;

View File

@ -0,0 +1,5 @@
begin;
alter table subtitles add column is_hearing_impaired boolean not null default false;
commit;

View File

@ -123,6 +123,8 @@ type Subtitle struct {
IsDefault bool `json:"isDefault"` IsDefault bool `json:"isDefault"`
/// Is this stream tagged as forced? /// Is this stream tagged as forced?
IsForced bool `json:"isForced"` 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) /// Is this an external subtitle (as in stored in a different file)
IsExternal bool `json:"isExternal"` IsExternal bool `json:"isExternal"`
/// Where the subtitle is stored (null if stored inside the video) /// Where the subtitle is stored (null if stored inside the video)
@ -294,6 +296,7 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) {
Extension: extension, Extension: extension,
IsDefault: stream.Disposition.Default != 0, IsDefault: stream.Disposition.Default != 0,
IsForced: stream.Disposition.Forced != 0, IsForced: stream.Disposition.Forced != 0,
IsHearingImpaired: stream.Disposition.HearingImpaired != 0,
Link: &link, Link: &link,
} }
}), }),

View File

@ -162,7 +162,7 @@ func (s *MetadataService) getMetadata(path string, sha string) (*MediaInfo, erro
} }
rows, err = s.database.Query( 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`, from subtitles as s where s.sha=$1`,
sha, sha,
) )
@ -171,7 +171,7 @@ func (s *MetadataService) getMetadata(path string, sha string) (*MediaInfo, erro
} }
for rows.Next() { for rows.Next() {
var s Subtitle 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 { if err != nil {
return nil, err return nil, err
} }
@ -273,8 +273,8 @@ func (s *MetadataService) storeFreshMetadata(path string, sha string) (*MediaInf
} }
for _, s := range ret.Subtitles { for _, s := range ret.Subtitles {
tx.Exec(` tx.Exec(`
insert into subtitles(sha, idx, title, language, codec, extension, is_default, is_forced) 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) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
on conflict (sha, idx) do update set on conflict (sha, idx) do update set
sha = excluded.sha, sha = excluded.sha,
idx = excluded.idx, idx = excluded.idx,
@ -283,9 +283,10 @@ func (s *MetadataService) storeFreshMetadata(path string, sha string) (*MediaInf
codec = excluded.codec, codec = excluded.codec,
extension = excluded.extension, extension = excluded.extension,
is_default = excluded.is_default, 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 { for _, c := range ret.Chapters {

View File

@ -43,26 +43,45 @@ outer:
Path: &match, Path: &match,
Link: &link, 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 // remove extension from flags
flags = flags[:len(flags)-1] flags = flags[:len(flags)-1]
for _, flag := range flags { for _, flag := range flags {
switch strings.ToLower(flag) { switch flag {
case "default": case "default":
sub.IsDefault = true sub.IsDefault = true
case "forced": case "forced":
sub.IsForced = true sub.IsForced = true
case "hi", "sdh", "cc":
sub.IsHearingImpaired = true
default: default:
lang, err := language.Parse(flag) lang, err := language.Parse(flag)
if err == nil && lang != language.Und { if err == nil && lang != language.Und {
lang := lang.String() langStr := lang.String()
sub.Language = &lang sub.Language = &langStr
} else { } else {
sub.Title = &flag 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) mi.Subtitles = append(mi.Subtitles, sub)
continue outer continue outer
} }

View File

@ -25,3 +25,14 @@ func Filter[E any](s []E, f func(E) bool) []E {
} }
return s2 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
}