mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Merge branch 'zoriya:master' into feature/helmchart
This commit is contained in:
commit
df07e75737
179
api/README.md
Normal file
179
api/README.md
Normal file
@ -0,0 +1,179 @@
|
||||
# Kyoo API
|
||||
|
||||
## Database schema
|
||||
|
||||
The many-to-many relation between entries (episodes/movies) & videos is NOT a mistake. Some video files can contain multiples episodes (like `MyShow 2&3.mvk`). One video file can also contain only a portion of an episode (like `MyShow 2 Part 1.mkv`)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
shows {
|
||||
guid id PK
|
||||
kind kind "serie|movie"
|
||||
string(128) slug UK
|
||||
genre[] genres
|
||||
int rating "From 0 to 100"
|
||||
status status "NN"
|
||||
datetime added_date
|
||||
date start_air
|
||||
date end_air "null for movies"
|
||||
datetime next_refresh
|
||||
jsonb external_id
|
||||
guid studio_id FK
|
||||
string original_language
|
||||
}
|
||||
show_translations {
|
||||
guid id PK, FK
|
||||
string language PK
|
||||
string name "NN"
|
||||
string tagline
|
||||
string[] aliases
|
||||
string description
|
||||
string[] tags
|
||||
string trailerUrl
|
||||
jsonb poster
|
||||
jsonb banner
|
||||
jsonb logo
|
||||
jsonb thumbnail
|
||||
}
|
||||
shows ||--|{ show_translations : has
|
||||
shows |o--|| entries : has
|
||||
|
||||
entries {
|
||||
guid id PK
|
||||
string(256) slug UK
|
||||
guid show_id FK, UK
|
||||
%% Order is absolute number.
|
||||
uint order "NN"
|
||||
uint season_number UK
|
||||
uint episode_number UK "NN"
|
||||
type type "episode|movie|special|extra"
|
||||
date air_date
|
||||
uint runtime
|
||||
jsonb thumbnail
|
||||
datetime next_refresh
|
||||
jsonb external_id
|
||||
}
|
||||
entry_translations {
|
||||
guid id PK, FK
|
||||
string language PK
|
||||
string name
|
||||
string description
|
||||
}
|
||||
entries ||--|{ entry_translations : has
|
||||
|
||||
video {
|
||||
guid id PK
|
||||
string path "NN"
|
||||
uint rendering "dedup for duplicates part1/2"
|
||||
uint part
|
||||
uint version "max version is preferred rendering"
|
||||
}
|
||||
video }|--|{ entries : for
|
||||
|
||||
collections {
|
||||
guid id PK
|
||||
string(256) slug UK
|
||||
datetime added_date
|
||||
datetime next_refresh
|
||||
}
|
||||
|
||||
collection_translations {
|
||||
guid id PK, FK
|
||||
string language PK
|
||||
string name "NN"
|
||||
jsonb poster
|
||||
jsonb thumbnail
|
||||
}
|
||||
collections ||--|{ collection_translations : has
|
||||
collections |o--|{ shows : has
|
||||
|
||||
seasons {
|
||||
guid id PK
|
||||
string(256) slug UK
|
||||
guid show_id FK
|
||||
uint season_number "NN"
|
||||
datetime added_date
|
||||
date start_air
|
||||
date end_air
|
||||
datetime next_refresh
|
||||
jsonb external_id
|
||||
}
|
||||
|
||||
season_translations {
|
||||
guid id PK,FK
|
||||
string language PK
|
||||
string name
|
||||
string description
|
||||
jsonb poster
|
||||
jsonb banner
|
||||
jsonb logo
|
||||
jsonb thumbnail
|
||||
}
|
||||
seasons ||--|{ season_translations : has
|
||||
seasons ||--o{ entries : has
|
||||
shows ||--|{ seasons : has
|
||||
|
||||
watched_shows {
|
||||
guid show_id PK, FK
|
||||
guid user_id PK, FK
|
||||
status status "completed|watching|droped|planned"
|
||||
uint seen_entry_count "NN"
|
||||
}
|
||||
shows ||--|{ watched_shows : has
|
||||
|
||||
watched_entries {
|
||||
guid entry_id PK, FK
|
||||
guid user_id PK, FK
|
||||
uint time "in seconds, null of finished"
|
||||
uint progress "NN, from 0 to 100"
|
||||
datetime played_date
|
||||
}
|
||||
entries ||--|{ watched_entries : has
|
||||
|
||||
roles {
|
||||
guid show_id PK, FK
|
||||
guid staff_id PK, FK
|
||||
uint order
|
||||
type type "actor|director|writer|producer|music|other"
|
||||
jsonb character_image
|
||||
}
|
||||
|
||||
role_translations {
|
||||
string language PK
|
||||
string character_name
|
||||
}
|
||||
roles||--o{ role_translations : has
|
||||
shows ||--|{ roles : has
|
||||
|
||||
staff {
|
||||
guid id PK
|
||||
string(256) slug UK
|
||||
jsonb image
|
||||
datetime next_refresh
|
||||
jsonb external_id
|
||||
}
|
||||
|
||||
staff_translations {
|
||||
guid id PK,FK
|
||||
string language PK
|
||||
string name "NN"
|
||||
}
|
||||
staff ||--|{ staff_translations : has
|
||||
staff ||--|{ roles : has
|
||||
|
||||
studios {
|
||||
guid id PK
|
||||
string(128) slug UK
|
||||
jsonb logo
|
||||
datetime next_refresh
|
||||
jsonb external_id
|
||||
}
|
||||
|
||||
studio_translations {
|
||||
guid id PK,FK
|
||||
string language PK
|
||||
string name
|
||||
}
|
||||
studios ||--|{ studio_translations : has
|
||||
shows ||--|{ studios : has
|
||||
```
|
@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "8.0.7",
|
||||
"version": "8.0.8",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Serilog" Version="4.0.1" />
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="3.0.2" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.8" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
@ -9,11 +9,11 @@
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
|
||||
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.1.4-prebeta" />
|
||||
<PackageReference Include="InterpolatedSql.Dapper" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.8" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -191,7 +191,7 @@ export const AccountProvider = ({
|
||||
<AccountContext.Provider value={accounts}>
|
||||
<ConnectionErrorContext.Provider
|
||||
value={{
|
||||
error: (selected ? initialSsrError.current ?? userError : null) ?? permissionError,
|
||||
error: (selected ? (initialSsrError.current ?? userError) : null) ?? permissionError,
|
||||
loading: userIsLoading,
|
||||
retry: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
||||
|
@ -61,7 +61,8 @@ export const SnackbarProvider = ({ children }: { children: ReactElement | ReactE
|
||||
return;
|
||||
}
|
||||
|
||||
addPortal("snackbar", <Snackbar {...top} />);
|
||||
const { key, ...props } = top;
|
||||
addPortal("snackbar", <Snackbar key={key} {...props} />);
|
||||
timeout.current = setTimeout(() => {
|
||||
removePortal("snackbar");
|
||||
updatePortal();
|
||||
|
@ -56,7 +56,7 @@ const MediaTypeTrigger = forwardRef<View, PressableProps & { mediaType: MediaTyp
|
||||
const { t } = useTranslation();
|
||||
const labelKey =
|
||||
mediaType !== MediaTypeAll ? `browse.mediatypekey.${mediaType.key}` : "browse.mediatypelabel";
|
||||
const icon = mediaType !== MediaTypeAll ? mediaType?.icon ?? FilterList : FilterList;
|
||||
const icon = mediaType !== MediaTypeAll ? (mediaType?.icon ?? FilterList) : FilterList;
|
||||
return (
|
||||
<PressableFeedback
|
||||
ref={ref}
|
||||
|
@ -53,11 +53,11 @@ export const itemMap = (
|
||||
href: item.href,
|
||||
poster: item.poster,
|
||||
thumbnail: item.thumbnail,
|
||||
watchStatus: item.kind !== "collection" ? item.watchStatus?.status ?? null : null,
|
||||
watchStatus: item.kind !== "collection" ? (item.watchStatus?.status ?? null) : null,
|
||||
type: item.kind,
|
||||
watchPercent: item.kind !== "collection" ? item.watchStatus?.watchedPercent ?? null : null,
|
||||
watchPercent: item.kind !== "collection" ? (item.watchStatus?.watchedPercent ?? null) : null,
|
||||
unseenEpisodesCount:
|
||||
item.kind === "show" ? item.watchStatus?.unseenEpisodesCount ?? item.episodesCount! : null,
|
||||
item.kind === "show" ? (item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!) : null,
|
||||
});
|
||||
|
||||
export const createFilterString = (mediaType: MediaType): string | undefined => {
|
||||
|
@ -178,7 +178,7 @@ export const CollectionPage: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
watchStatus={(item.kind !== "collection" && item.watchStatus?.status) || null}
|
||||
unseenEpisodesCount={
|
||||
item.kind === "show"
|
||||
? item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!
|
||||
? (item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!)
|
||||
: null
|
||||
}
|
||||
{...css({ marginX: ItemGrid.layout.gap })}
|
||||
|
@ -307,7 +307,7 @@ export const Recommended = () => {
|
||||
watchStatus={(item.kind !== "collection" && item.watchStatus?.status) || null}
|
||||
unseenEpisodesCount={
|
||||
item.kind === "show"
|
||||
? item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!
|
||||
? (item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
|
@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
import Svg, { type SvgProps, Path } from "react-native-svg";
|
||||
import { useYoshiki } from "yoshiki";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
|
||||
/* export const KyooLogo = (props: SvgProps) => ( */
|
||||
/* <Svg viewBox="0 0 128 128" {...props}> */
|
||||
|
@ -92,7 +92,8 @@ export const Player = ({
|
||||
const [playbackError, setPlaybackError] = useState<string | undefined>(undefined);
|
||||
const { data, error } = useFetch(Player.query(type, slug));
|
||||
const { data: info, error: infoError } = useFetch(Player.infoQuery(type, slug));
|
||||
const image = data && data.type === "episode" ? data.show?.poster ?? data?.poster : data?.poster;
|
||||
const image =
|
||||
data && data.type === "episode" ? (data.show?.poster ?? data?.poster) : data?.poster;
|
||||
const previous =
|
||||
data && data.type === "episode" && data.previousEpisode
|
||||
? `/watch/${data.previousEpisode.slug}?t=0`
|
||||
|
@ -147,7 +147,7 @@ export const AudiosMenu = ({ audios, ...props }: CustomMenu & { audios?: Audio[]
|
||||
{info.audioTracks.map((x) => (
|
||||
<Menu.Item
|
||||
key={x.index}
|
||||
label={audios ? getDisplayName(audios[x.index]) : x.title ?? x.language ?? "Unknown"}
|
||||
label={audios ? getDisplayName(audios[x.index]) : (x.title ?? x.language ?? "Unknown")}
|
||||
selected={audio!.index === x.index}
|
||||
onSelect={() => setAudio(x as any)}
|
||||
/>
|
||||
|
@ -71,7 +71,7 @@ export const GeneralSettings = () => {
|
||||
onValueChange={(value) => changeLanguage(value)}
|
||||
values={["system", ...Object.keys(i18n.options.resources!)]}
|
||||
getLabel={(key) =>
|
||||
key === "system" ? t("settings.general.language.system") : getLanguageName(key) ?? key
|
||||
key === "system" ? t("settings.general.language.system") : (getLanguageName(key) ?? key)
|
||||
}
|
||||
/>
|
||||
</Preference>
|
||||
|
@ -64,7 +64,7 @@ export const PlaybackSettings = () => {
|
||||
onValueChange={(value) => setAudio(value)}
|
||||
values={["default", ...allLanguages]}
|
||||
getLabel={(key) =>
|
||||
key === "default" ? t("mediainfo.default") : getLanguageName(key) ?? key
|
||||
key === "default" ? t("mediainfo.default") : (getLanguageName(key) ?? key)
|
||||
}
|
||||
/>
|
||||
</Preference>
|
||||
@ -83,7 +83,7 @@ export const PlaybackSettings = () => {
|
||||
? t("settings.playback.subtitleLanguage.none")
|
||||
: key === "default"
|
||||
? t("mediainfo.default")
|
||||
: getLanguageName(key) ?? key
|
||||
: (getLanguageName(key) ?? key)
|
||||
}
|
||||
/>
|
||||
</Preference>
|
||||
|
285
front/translations/es.json
Normal file
285
front/translations/es.json
Normal file
@ -0,0 +1,285 @@
|
||||
{
|
||||
"home": {
|
||||
"recommended": "Recomendado",
|
||||
"news": "Noticias",
|
||||
"watchlist": "Continuar viendo",
|
||||
"info": "Ver mas",
|
||||
"none": "Sin episodios",
|
||||
"watchlistLogin": "Para hacer un seguimiento de lo que has visto o piensas ver, tienes que iniciar sesión.",
|
||||
"refreshMetadata": "Refrescar metadatos",
|
||||
"episodeMore": {
|
||||
"goToShow": "Ir al show",
|
||||
"download": "Descarga",
|
||||
"mediainfo": "Ver información del archivo"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"play": "Reproducir",
|
||||
"trailer": "Ver el tráiler",
|
||||
"studio": "Estudio",
|
||||
"genre": "Géneros",
|
||||
"genre-none": "Sin géneros",
|
||||
"staff": "Equipo",
|
||||
"staff-none": "El equipo es desconocido",
|
||||
"noOverview": "Resumen no disponible",
|
||||
"episode-none": "No hay episodios en esta temporada",
|
||||
"episodeNoMetadata": "Metadatos no disponibles",
|
||||
"tags": "Etiquetas",
|
||||
"links": "Enlaces",
|
||||
"jumpToSeason": "Ir a temporada",
|
||||
"partOf": "Parte de la",
|
||||
"watchlistAdd": "Agregar a su lista de seguimiento",
|
||||
"watchlistEdit": "Modificar el estado de visto",
|
||||
"watchlistRemove": "Marcar como no visto",
|
||||
"watchlistLogin": "Inicia sesión para añadir a tu lista de seguimiento",
|
||||
"watchlistMark": {
|
||||
"completed": "Marcar como completado",
|
||||
"planned": "Marcar como planificado",
|
||||
"watching": "Marcar como viendo",
|
||||
"droped": "Marcar como descartado",
|
||||
"null": "Marcar como no visto"
|
||||
},
|
||||
"nextUp": "Siguiente",
|
||||
"season": "Temporada {{number}}"
|
||||
},
|
||||
"browse": {
|
||||
"mediatypekey": {
|
||||
"all": "Todo",
|
||||
"movie": "Películas",
|
||||
"show": "Programas de TV",
|
||||
"collection": "Colección"
|
||||
},
|
||||
"mediatype-tt": "Tipos de medios",
|
||||
"mediatypelabel": "Tipo de medio",
|
||||
"sortby": "Ordenar por {{key}}",
|
||||
"sortby-tt": "Ordenar por",
|
||||
"sortkey": {
|
||||
"relevance": "Relevancia",
|
||||
"name": "Nombre",
|
||||
"airDate": "Fecha de emisión",
|
||||
"startAir": "Empieza emisión",
|
||||
"endAir": "Finaliza emisión",
|
||||
"addedDate": "Fecha de incorporación",
|
||||
"rating": "Calificaciones"
|
||||
},
|
||||
"sortord": {
|
||||
"asc": "ascendente",
|
||||
"desc": "descendente"
|
||||
},
|
||||
"switchToGrid": "Cambiar a vista de cuadrícula",
|
||||
"switchToList": "Cambiar a vista de lista"
|
||||
},
|
||||
"genres": {
|
||||
"Action": "Acción",
|
||||
"Adventure": "Aventura",
|
||||
"Animation": "Animación",
|
||||
"Comedy": "Comedia",
|
||||
"Crime": "Crimen",
|
||||
"Documentary": "Documental",
|
||||
"Drama": "Drama",
|
||||
"Family": "Familia",
|
||||
"Fantasy": "Fantasía",
|
||||
"History": "Historia",
|
||||
"Horror": "Horror",
|
||||
"Music": "Musica",
|
||||
"Mystery": "Misterio",
|
||||
"Romance": "Romance",
|
||||
"ScienceFiction": "Ciencia ficción",
|
||||
"Thriller": "Suspenso",
|
||||
"War": "Bélica",
|
||||
"Western": "Del oeste",
|
||||
"Kids": "Niños",
|
||||
"News": "Noticias",
|
||||
"Reality": "Realidad",
|
||||
"Soap": "Novela",
|
||||
"Talk": "Entrevista",
|
||||
"Politics": "Política"
|
||||
},
|
||||
"misc": {
|
||||
"settings": "Ajustes",
|
||||
"prev-page": "Pagina anterior",
|
||||
"next-page": "Pagina siguiente",
|
||||
"delete": "Eliminar",
|
||||
"cancel": "Cancelar",
|
||||
"more": "Mas",
|
||||
"expand": "Agrandar",
|
||||
"collapse": "Expandir",
|
||||
"edit": "Modificar",
|
||||
"or": "O",
|
||||
"loading": "Cargando"
|
||||
},
|
||||
"navbar": {
|
||||
"home": "Principal",
|
||||
"browse": "Navegar",
|
||||
"download": "Descargar",
|
||||
"search": "Buscar",
|
||||
"login": "Acceder",
|
||||
"admin": "Panel de administración"
|
||||
},
|
||||
"settings": {
|
||||
"general": {
|
||||
"label": "General",
|
||||
"theme": {
|
||||
"label": "Tema",
|
||||
"description": "Establece el tema de tu aplicación",
|
||||
"auto": "Sistema",
|
||||
"light": "Claro",
|
||||
"dark": "Oscuro"
|
||||
},
|
||||
"language": {
|
||||
"label": "Idioma",
|
||||
"description": "Establecer el idioma de tu aplicación",
|
||||
"system": "Sistema"
|
||||
}
|
||||
},
|
||||
"playback": {
|
||||
"label": "Reproducción",
|
||||
"playmode": {
|
||||
"label": "Modo de reproducción predeterminado",
|
||||
"description": "El modo de reproducción predeterminado utilizado con este cliente. La calidad original requiere menos recursos en el servidor, pero no permite cambios automáticos de calidad"
|
||||
},
|
||||
"audioLanguage": {
|
||||
"label": "Idioma del audio",
|
||||
"description": "El idioma del audio predeterminado utilizado al reproducir videos con múltiples pistas de audio"
|
||||
},
|
||||
"subtitleLanguage": {
|
||||
"label": "Idioma de los subtítulos",
|
||||
"description": "Idioma de los subtítulos utilizado por defecto",
|
||||
"none": "Ninguno"
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"label": "Cuenta",
|
||||
"username": {
|
||||
"label": "Nombre de usuario"
|
||||
},
|
||||
"avatar": {
|
||||
"label": "Avatar",
|
||||
"description": "Cambiar icono del perfil"
|
||||
},
|
||||
"email": {
|
||||
"label": "Correo electrónico"
|
||||
},
|
||||
"password": {
|
||||
"label": "Contraseña",
|
||||
"description": "Cambiar tu contraseña",
|
||||
"oldPassword": "Contraseña anterior",
|
||||
"newPassword": "Contraseña nueva"
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"label": "Cuentas vinculadas",
|
||||
"connected": "Conectad@ como {{username}}.",
|
||||
"not-connected": "Desconectad@",
|
||||
"open-profile": "Abre tu perfil de {{provider}}",
|
||||
"link": "Enlace",
|
||||
"delete": "Desvincula tu cuenta de kyoo con tu cuenta de {{provider}}"
|
||||
},
|
||||
"about": {
|
||||
"label": "Acerca de",
|
||||
"android-app": {
|
||||
"label": "Aplicación de Android",
|
||||
"description": "Descarga la aplicación de Android"
|
||||
},
|
||||
"git": {
|
||||
"label": "Github",
|
||||
"description": "Abre el repositorio de GitHub donde puedes explorar el código de Kyoo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"back": "Atrás",
|
||||
"previous": "Episodio anterior",
|
||||
"next": "Siguiente episodio",
|
||||
"play": "Iniciar",
|
||||
"pause": "Pausar",
|
||||
"mute": "Alternar silencio",
|
||||
"volume": "Volumen",
|
||||
"quality": "Calidad",
|
||||
"audios": "Audio",
|
||||
"subtitles": "Subtítulos",
|
||||
"subtitle-none": "Ninguno",
|
||||
"fullscreen": "Pantalla completa",
|
||||
"direct": "Prístino",
|
||||
"transmux": "Original",
|
||||
"auto": "Auto",
|
||||
"notInPristine": "No disponible en prístino",
|
||||
"unsupportedError": "Códec de video no compatible, transcodificación en progreso..."
|
||||
},
|
||||
"search": {
|
||||
"empty": "Sin resultados. Intente otra búsqueda."
|
||||
},
|
||||
"login": {
|
||||
"login": "Iniciar sesión",
|
||||
"register": "Registrarse",
|
||||
"guest": "Continuar como invitado",
|
||||
"guest-forbidden": "Esta instancia de Kyoo no permite invitados.",
|
||||
"via": "Continuar con {{provider}}",
|
||||
"add-account": "Agregar cuenta",
|
||||
"logout": "Desconectarse",
|
||||
"server": "Dirección del servidor",
|
||||
"email": "Correo electrónico",
|
||||
"username": "Usuario",
|
||||
"password": "Contraseña",
|
||||
"confirm": "Confirmar Contraseña",
|
||||
"or-register": "¿No tienes una cuenta? <1>Regístrate</1>.",
|
||||
"or-login": "¿Ya tienes una cuenta? <1>Iniciar sesión</1>.",
|
||||
"password-no-match": "La contraseña no coincide.",
|
||||
"delete": "Elimine su cuenta",
|
||||
"delete-confirmation": "Esta acción no se puede deshacer. ¿Está seguro?"
|
||||
},
|
||||
"downloads": {
|
||||
"empty": "Nada descargado aún, empieza a buscar algo que te guste",
|
||||
"error": "Error: {{error}}",
|
||||
"delete": "Eliminar elemento",
|
||||
"deleteMessage": "¿Quieres eliminar este elemento de tu almacenamiento local?",
|
||||
"pause": "Pausa",
|
||||
"resume": "Reanudar",
|
||||
"retry": "Reintentar"
|
||||
},
|
||||
"errors": {
|
||||
"connection": "No se pudo conectar al servidor de Kyoo",
|
||||
"connection-tips": "Consejos para solucionar problemas:\n - ¿Estás conectado a Internet?\n - ¿Está en línea tu servidor de kyoo?\n - ¿Tu cuenta ha sido baneada?",
|
||||
"unknown": "Error desconocido",
|
||||
"try-again": "Intentar nuevamente",
|
||||
"re-login": "Reiniciar sesión",
|
||||
"offline": "No estás conectado a internet. Intenta de nuevo más tarde.",
|
||||
"unauthorized": "Faltan los permisos {{permission}} para acceder a esta página.",
|
||||
"needVerification": "Tu cuenta debe ser verificada por el administrador del servidor antes de que puedas usarla.",
|
||||
"needAccount": "Esta página no se puede acceder en modo invitado. Necesitas crear una cuenta o iniciar sesión.",
|
||||
"setup": {
|
||||
"MissingAdminAccount": "Aún no se ha creado ninguna cuenta de administrador. Por favor, regístrate para crear una.",
|
||||
"NoVideoFound": "No se encontró ningún video aún. ¡Añade películas o series dentro de la carpeta de tu biblioteca para que se muestren aquí!"
|
||||
}
|
||||
},
|
||||
"mediainfo": {
|
||||
"file": "Archivo",
|
||||
"container": "Recipiente",
|
||||
"video": "Video",
|
||||
"audio": "Audio",
|
||||
"subtitles": "Subtítulos",
|
||||
"forced": "Forzado",
|
||||
"default": "Predeterminado",
|
||||
"external": "Externo",
|
||||
"duration": "Duración",
|
||||
"size": "Tamaño",
|
||||
"novideo": "Sin vídeo",
|
||||
"nocontainer": "Contenedor invalido"
|
||||
},
|
||||
"admin": {
|
||||
"users": {
|
||||
"label": "Usuarios",
|
||||
"adminUser": "Administrador",
|
||||
"regularUser": "Usuario",
|
||||
"set-permissions": "Establecer permisos",
|
||||
"delete": "Eliminar usuario",
|
||||
"unverifed": "Sin verificar",
|
||||
"verify": "Verificar usuario"
|
||||
},
|
||||
"scanner": {
|
||||
"label": "Escáner",
|
||||
"scan": "Iniciar escaneo de la biblioteca",
|
||||
"empty": "No se encontraron problemas. Todos tus elementos están registrados."
|
||||
}
|
||||
}
|
||||
}
|
@ -1,54 +1,54 @@
|
||||
{
|
||||
"home": {
|
||||
"recommended": "",
|
||||
"recommended": "Raccomandati",
|
||||
"news": "",
|
||||
"watchlist": "",
|
||||
"watchlist": "Continua a guardare",
|
||||
"info": "",
|
||||
"none": "",
|
||||
"none": "Nessun episodio",
|
||||
"watchlistLogin": "",
|
||||
"refreshMetadata": "",
|
||||
"refreshMetadata": "Aggiorna metadata",
|
||||
"episodeMore": {
|
||||
"goToShow": "",
|
||||
"goToShow": "Vai allo show",
|
||||
"download": "",
|
||||
"mediainfo": ""
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"play": "",
|
||||
"trailer": "",
|
||||
"play": "Riproduci",
|
||||
"trailer": "Riproduci Trailer",
|
||||
"studio": "",
|
||||
"genre": "",
|
||||
"genre-none": "",
|
||||
"genre": "Generi",
|
||||
"genre-none": "Nessun genere",
|
||||
"staff": "",
|
||||
"staff-none": "",
|
||||
"noOverview": "",
|
||||
"episode-none": "",
|
||||
"episode-none": "Non ci sono episodi in questa stagione",
|
||||
"episodeNoMetadata": "",
|
||||
"tags": "",
|
||||
"links": "",
|
||||
"links": "Link",
|
||||
"jumpToSeason": "",
|
||||
"partOf": "",
|
||||
"watchlistAdd": "",
|
||||
"watchlistEdit": "",
|
||||
"watchlistRemove": "",
|
||||
"watchlistRemove": "Segna come non visto",
|
||||
"watchlistLogin": "",
|
||||
"watchlistMark": {
|
||||
"completed": "",
|
||||
"completed": "Segna come completato",
|
||||
"planned": "",
|
||||
"watching": "",
|
||||
"droped": "",
|
||||
"null": ""
|
||||
"null": "Segna come non visto"
|
||||
},
|
||||
"nextUp": "",
|
||||
"season": ""
|
||||
"nextUp": "Prossimo",
|
||||
"season": "Stagione {{number}}"
|
||||
},
|
||||
"browse": {
|
||||
"sortby": "",
|
||||
"sortby-tt": "",
|
||||
"sortby": "Ordina per {{key}}",
|
||||
"sortby-tt": "Ordina per",
|
||||
"sortkey": {
|
||||
"relevance": "",
|
||||
"name": "",
|
||||
"airDate": "",
|
||||
"name": "Nome",
|
||||
"airDate": "Data di trasmissione",
|
||||
"startAir": "",
|
||||
"endAir": "",
|
||||
"addedDate": "",
|
||||
@ -59,46 +59,52 @@
|
||||
"desc": ""
|
||||
},
|
||||
"switchToGrid": "",
|
||||
"switchToList": ""
|
||||
"switchToList": "",
|
||||
"mediatypekey": {
|
||||
"all": "Tutti",
|
||||
"movie": "Film",
|
||||
"show": "Serie TV",
|
||||
"collection": "Collezione"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"settings": "",
|
||||
"prev-page": "",
|
||||
"next-page": "",
|
||||
"delete": "",
|
||||
"cancel": "",
|
||||
"more": "",
|
||||
"expand": "",
|
||||
"settings": "Impostazioni",
|
||||
"prev-page": "Pagina precedente",
|
||||
"next-page": "Pagina successiva",
|
||||
"delete": "Elimina",
|
||||
"cancel": "Annulla",
|
||||
"more": "Di più",
|
||||
"expand": "Espandi",
|
||||
"collapse": "",
|
||||
"edit": "",
|
||||
"edit": "Modifica",
|
||||
"or": "",
|
||||
"loading": ""
|
||||
"loading": "Caricamento"
|
||||
},
|
||||
"navbar": {
|
||||
"home": "",
|
||||
"browse": "",
|
||||
"search": "",
|
||||
"login": "",
|
||||
"admin": ""
|
||||
"home": "Home",
|
||||
"browse": "Cerca",
|
||||
"search": "Ricerca",
|
||||
"login": "Login",
|
||||
"admin": "Pannello amministratore"
|
||||
},
|
||||
"settings": {
|
||||
"general": {
|
||||
"label": "",
|
||||
"label": "Generale",
|
||||
"theme": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
"auto": "",
|
||||
"light": "",
|
||||
"dark": ""
|
||||
"label": "Tema",
|
||||
"description": "Imposta il tema della tua applicazione",
|
||||
"auto": "Sistema",
|
||||
"light": "Chiaro",
|
||||
"dark": "Scuro"
|
||||
},
|
||||
"language": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
"system": ""
|
||||
"label": "Lingua",
|
||||
"description": "Imposta la lingua della tua applicazione",
|
||||
"system": "Sistema"
|
||||
}
|
||||
},
|
||||
"playback": {
|
||||
"label": "",
|
||||
"label": "Riproduzione",
|
||||
"playmode": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
@ -241,5 +247,22 @@
|
||||
"scan": "",
|
||||
"empty": ""
|
||||
}
|
||||
},
|
||||
"genres": {
|
||||
"Mystery": "Mistero",
|
||||
"Kids": "Bambini",
|
||||
"Western": "Western",
|
||||
"History": "Storico",
|
||||
"Romance": "Romantico",
|
||||
"ScienceFiction": "Fantascienza",
|
||||
"Thriller": "Thriller",
|
||||
"War": "Guerra",
|
||||
"Animation": "Animazione",
|
||||
"Action": "Azione",
|
||||
"Adventure": "Avventura",
|
||||
"Comedy": "Commedia",
|
||||
"Crime": "Criminale",
|
||||
"Documentary": "Documentario",
|
||||
"Drama": "Drammatico"
|
||||
}
|
||||
}
|
||||
|
@ -237,7 +237,8 @@
|
||||
"duration": "Uzunluk",
|
||||
"size": "Boyut",
|
||||
"novideo": "Video yok",
|
||||
"nocontainer": "Geçersiz kapsayıcı"
|
||||
"nocontainer": "Geçersiz kapsayıcı",
|
||||
"external": "Harici"
|
||||
},
|
||||
"admin": {
|
||||
"users": {
|
||||
|
@ -26,6 +26,9 @@ class KyooClient:
|
||||
async def __aenter__(self):
|
||||
jsons.set_serializer(lambda x, **_: format_date(x), type[Optional[date | int]])
|
||||
self.client = ClientSession(
|
||||
headers={
|
||||
"User-Agent": "kyoo",
|
||||
},
|
||||
json_serialize=lambda *args, **kwargs: jsons.dumps(
|
||||
*args, key_transformer=jsons.KEY_TRANSFORMER_CAMELCASE, **kwargs
|
||||
),
|
||||
|
@ -26,4 +26,7 @@ POSTGRES_PASSWORD=
|
||||
POSTGRES_DB=
|
||||
POSTGRES_SERVER=
|
||||
POSTGRES_PORT=5432
|
||||
# (the schema "gocoder" will be used)
|
||||
# Default is gocoder, you can specify "disabled" to use the default search_path of the user.
|
||||
# If this is not "disabled", the schema will be created (if it does not exists) and
|
||||
# the search_path of the user will be ignored (only the schema specified will be used).
|
||||
POSTGRES_SCHEMA=gocoder
|
||||
|
@ -1,9 +1,9 @@
|
||||
# FROM golang:1.22 as build
|
||||
FROM debian:trixie-slim as build
|
||||
FROM debian:trixie-slim AS build
|
||||
# those were copied from https://github.com/docker-library/golang/blob/master/Dockerfile-linux.template
|
||||
ENV GOTOOLCHAIN=local
|
||||
ENV GOPATH /go
|
||||
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
|
||||
ENV GOPATH=/go
|
||||
ENV PATH=$GOPATH/bin:/usr/local/go/bin:$PATH
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
|
@ -5,8 +5,8 @@
|
||||
FROM debian:trixie-slim
|
||||
# those were copied from https://github.com/docker-library/golang/blob/master/Dockerfile-linux.template
|
||||
ENV GOTOOLCHAIN=local
|
||||
ENV GOPATH /go
|
||||
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
|
||||
ENV GOPATH=/go
|
||||
ENV PATH=$GOPATH/bin:/usr/local/go/bin:$PATH
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
|
@ -25,6 +25,6 @@ require (
|
||||
golang.org/x/image v0.19.0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sys v0.23.0 // indirect
|
||||
golang.org/x/text v0.17.0
|
||||
golang.org/x/text v0.18.0
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
)
|
||||
|
@ -76,8 +76,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
@ -36,7 +37,7 @@ type MediaInfo struct {
|
||||
/// The file size of the video file.
|
||||
Size int64 `json:"size"`
|
||||
/// The length of the media in seconds.
|
||||
Duration float32 `json:"duration"`
|
||||
Duration float64 `json:"duration"`
|
||||
/// The container of the video file of this episode.
|
||||
Container *string `json:"container"`
|
||||
/// Version of the metadata. This can be used to invalidate older metadata from db if the extraction code has changed.
|
||||
@ -55,6 +56,9 @@ type MediaInfo struct {
|
||||
Fonts []string `json:"fonts"`
|
||||
/// The list of chapters. See Chapter for more information.
|
||||
Chapters []Chapter `json:"chapters"`
|
||||
|
||||
/// lock used to read/set keyframes of video/audio
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
type Video struct {
|
||||
@ -238,7 +242,7 @@ func RetriveMediaInfo(path string, sha string) (*MediaInfo, error) {
|
||||
// Remove leading .
|
||||
Extension: filepath.Ext(path)[1:],
|
||||
Size: ParseInt64(mi.Format.Size),
|
||||
Duration: float32(mi.Format.DurationSeconds),
|
||||
Duration: mi.Format.DurationSeconds,
|
||||
Container: OrNull(mi.Format.FormatName),
|
||||
Versions: Versions{
|
||||
Info: InfoVersion,
|
||||
|
@ -2,6 +2,7 @@ package src
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
@ -88,11 +89,17 @@ type KeyframeKey struct {
|
||||
}
|
||||
|
||||
func (s *MetadataService) GetKeyframes(info *MediaInfo, isVideo bool, idx uint32) (*Keyframe, error) {
|
||||
info.lock.Lock()
|
||||
var ret *Keyframe
|
||||
if isVideo && info.Videos[idx].Keyframes != nil {
|
||||
return info.Videos[idx].Keyframes, nil
|
||||
ret = info.Videos[idx].Keyframes
|
||||
}
|
||||
if !isVideo && info.Audios[idx].Keyframes != nil {
|
||||
return info.Audios[idx].Keyframes, nil
|
||||
ret = info.Audios[idx].Keyframes
|
||||
}
|
||||
info.lock.Unlock()
|
||||
if ret != nil {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
get_running, set := s.keyframeLock.Start(KeyframeKey{
|
||||
@ -110,6 +117,14 @@ func (s *MetadataService) GetKeyframes(info *MediaInfo, isVideo bool, idx uint32
|
||||
}
|
||||
kf.info.ready.Add(1)
|
||||
|
||||
info.lock.Lock()
|
||||
if isVideo {
|
||||
info.Videos[idx].Keyframes = kf
|
||||
} else {
|
||||
info.Audios[idx].Keyframes = kf
|
||||
}
|
||||
info.lock.Unlock()
|
||||
|
||||
go func() {
|
||||
var table string
|
||||
var err error
|
||||
@ -122,7 +137,7 @@ func (s *MetadataService) GetKeyframes(info *MediaInfo, isVideo bool, idx uint32
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Couldn't retrive keyframes for %s %s %d: %v", info.Path, table, idx, err)
|
||||
log.Printf("Couldn't retrieve keyframes for %s %s %d: %v", info.Path, table, idx, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -157,6 +172,8 @@ func getVideoKeyframes(path string, video_idx uint32, kf *Keyframe) error {
|
||||
"-loglevel", "error",
|
||||
"-select_streams", fmt.Sprintf("V:%d", video_idx),
|
||||
"-show_entries", "packet=pts_time,flags",
|
||||
// some avi files don't have pts, we use this to ask ffmpeg to generate them (it uses the dts under the hood)
|
||||
"-fflags", "+genpts",
|
||||
"-of", "csv=print_section=0",
|
||||
path,
|
||||
)
|
||||
@ -191,6 +208,8 @@ func getVideoKeyframes(path string, video_idx uint32, kf *Keyframe) error {
|
||||
pts, flags := x[0], x[1]
|
||||
|
||||
// true if there is no keyframes (e.g. in a file w/o video track)
|
||||
// can also happen if a video has more packets than frames (so the last packet
|
||||
// is emtpy and has a N/A pts)
|
||||
if pts == "N/A" {
|
||||
break
|
||||
}
|
||||
@ -231,16 +250,89 @@ func getVideoKeyframes(path string, video_idx uint32, kf *Keyframe) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const DummyKeyframeDuration = float64(4)
|
||||
|
||||
// we can pretty much cut audio at any point so no need to get specific frames, just cut every 4s
|
||||
func getAudioKeyframes(info *MediaInfo, audio_idx uint32, kf *Keyframe) error {
|
||||
dummyKeyframeDuration := float64(4)
|
||||
segmentCount := int((float64(info.Duration) / dummyKeyframeDuration) + 1)
|
||||
kf.Keyframes = make([]float64, segmentCount)
|
||||
for segmentIndex := 0; segmentIndex < segmentCount; segmentIndex += 1 {
|
||||
kf.Keyframes[segmentIndex] = float64(segmentIndex) * dummyKeyframeDuration
|
||||
defer printExecTime("ffprobe keyframe analysis for %s audio n%d", info.Path, audio_idx)()
|
||||
// Format's duration CAN be different than audio's duration. To make sure we do not
|
||||
// miss a segment or make one more, we need to check the audio's duration.
|
||||
//
|
||||
// Since fetching the duration requires reading packets and is SLOW, we start by generating
|
||||
// keyframes until a reasonably safe point of the file (if the format has a 20min duration, audio
|
||||
// probably has a close duration).
|
||||
// You can read why duration retrieval is slow on the comment below.
|
||||
safe_duration := info.Duration - 20
|
||||
segment_count := int((safe_duration / DummyKeyframeDuration) + 1)
|
||||
if segment_count > 0 {
|
||||
kf.Keyframes = make([]float64, segment_count)
|
||||
for i := 0; i < segment_count; i += 1 {
|
||||
kf.Keyframes[i] = float64(i) * DummyKeyframeDuration
|
||||
}
|
||||
kf.info.ready.Done()
|
||||
} else {
|
||||
segment_count = 0
|
||||
}
|
||||
|
||||
// Some formats DO NOT contain a duration metadata, we need to manually fetch it
|
||||
// from the packets.
|
||||
//
|
||||
// We could use the same command to retrieve all packets and know when we can cut PRECISELY
|
||||
// but since packets always contain only a few ms we don't need this precision.
|
||||
cmd := exec.Command(
|
||||
"ffprobe",
|
||||
"-select_streams", fmt.Sprintf("a:%d", audio_idx),
|
||||
"-show_entries", "packet=pts_time",
|
||||
// some avi files don't have pts, we use this to ask ffmpeg to generate them (it uses the dts under the hood)
|
||||
"-fflags", "+genpts",
|
||||
// We use a read_interval LARGER than the file (at least we estimate)
|
||||
// This allows us to only decode the LAST packets
|
||||
"-read_intervals", fmt.Sprintf("%f", info.Duration+10_000),
|
||||
"-of", "csv=print_section=0",
|
||||
info.Path,
|
||||
)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
var duration float64
|
||||
for scanner.Scan() {
|
||||
pts := scanner.Text()
|
||||
if pts == "" || pts == "N/A" {
|
||||
continue
|
||||
}
|
||||
|
||||
duration, err = strconv.ParseFloat(pts, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if duration <= 0 {
|
||||
return errors.New("could not find audio's duration")
|
||||
}
|
||||
|
||||
new_seg_count := int((duration / DummyKeyframeDuration) + 1)
|
||||
if new_seg_count > segment_count {
|
||||
new_segments := make([]float64, new_seg_count-segment_count)
|
||||
for i := segment_count; i < new_seg_count; i += 1 {
|
||||
new_segments[i-segment_count] = float64(i) * DummyKeyframeDuration
|
||||
}
|
||||
kf.add(new_segments)
|
||||
if segment_count == 0 {
|
||||
kf.info.ready.Done()
|
||||
}
|
||||
}
|
||||
|
||||
kf.IsDone = true
|
||||
kf.info.ready.Done()
|
||||
return nil
|
||||
}
|
||||
|
@ -24,21 +24,28 @@ type MetadataService struct {
|
||||
|
||||
func NewMetadataService() (*MetadataService, error) {
|
||||
con := fmt.Sprintf(
|
||||
"postgresql://%v:%v@%v:%v/%v?application_name=gocoder&search_path=gocoder&sslmode=disable",
|
||||
"postgresql://%v:%v@%v:%v/%v?application_name=gocoder&sslmode=disable",
|
||||
url.QueryEscape(os.Getenv("POSTGRES_USER")),
|
||||
url.QueryEscape(os.Getenv("POSTGRES_PASSWORD")),
|
||||
url.QueryEscape(os.Getenv("POSTGRES_SERVER")),
|
||||
url.QueryEscape(os.Getenv("POSTGRES_PORT")),
|
||||
url.QueryEscape(os.Getenv("POSTGRES_DB")),
|
||||
)
|
||||
schema := GetEnvOr("POSTGRES_SCHEMA", "gocoder")
|
||||
if schema != "disabled" {
|
||||
con = fmt.Sprintf("%s&search_path=%s", con, url.QueryEscape(schema))
|
||||
}
|
||||
db, err := sql.Open("postgres", con)
|
||||
if err != nil {
|
||||
fmt.Printf("Could not connect to database, check your env variables!")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = db.Exec("create schema if not exists gocoder")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if schema != "disabled" {
|
||||
_, err = db.Exec(fmt.Sprintf("create schema if not exists %s", schema))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
driver, err := postgres.WithInstance(db, &postgres.Config{})
|
||||
|
@ -238,6 +238,11 @@ func (ts *Stream) run(start int32) error {
|
||||
)
|
||||
}
|
||||
args = append(args,
|
||||
// some avi files are missing pts, using this flag makes ffmpeg use dts as pts and prevents an error with
|
||||
// -c:v copy. Only issue: pts is sometime wrong (+1fps than expected) and this leads to some clients refusing
|
||||
// to play the file (they just switch back to the previous quality).
|
||||
// since this is better than errorring or not supporting transmux at all, i'll keep it here for now.
|
||||
"-fflags", "+genpts",
|
||||
"-i", ts.file.Info.Path,
|
||||
// this makes behaviors consistent between soft and hardware decodes.
|
||||
// this also means that after a -ss 50, the output video will start at 50s
|
||||
|
Loading…
x
Reference in New Issue
Block a user