Files
euzuandGitHub 91f6c57159 Fix/merge live and vod recording (#876)
feat(dvr)!: unify live and VOD/series recording into one recoverable DVR

Merge fix/merge_live_and_vod_recording into develop.

- One recording queue for scheduled live captures and VOD/series transfers,
  with a shared transition graph, provider-slot scheduling, resumable HTTP
  transfers (strong ETag / Last-Modified validation) and ffmpeg live capture.
- Crash-recoverable persistence: B+Tree repository with a journal-first
  recovery history; one file can be shared by several library entries.
- Admission checks quota and disk on request, at transfer start (real size)
  and before an entry attaches to a finished file; idempotent requests.
- REST is command-only; state reaches clients via owner-filtered WebSocket
  snapshots.
- Deleting a recording removes the file once no entry holds it; removing an
  entry keeps a finished file and cleans up orphaned partials.
- User ids derive from the configured username (web:/api:); the identity
  registry is gone.
- Spanish locale added; ru/ar completed; locale parity test.

BREAKING CHANGE: `video.download` is now `video.recording` (nested
`recording` keys move up, `download_priority`/`recording_priority` become
`priority`); old configs fail to load with a migration hint.
BREAKING CHANGE: `/api/v1/file/download*` and `/file/record` are removed in
favour of `/api/v1/recording/*`.
BREAKING CHANGE: `download.*` and `recording.write` permissions are replaced
by `recording.read|create|manage|delete`; tokens from before the change must
re-authenticate.
BREAKING CHANGE: the old recording queue does not migrate; existing
`identity_registry.json` is ignored.
2026-10-04 16:02:55 +02:00

412 lines
8.4 KiB
SCSS

@use "../../../size";
.tp__streams {
display: flex;
flex-flow: column;
flex: 1 1 auto;
min-height: 0;
box-sizing: border-box;
width: 100%;
overflow: hidden;
&__header {
display: flex;
flex-flow: row nowrap;
}
&__body {
display: flex;
flex-flow: column;
flex: 1 1 auto;
min-height: 0;
gap: var(--gap-default);
overflow: auto;
pointer-events: auto;
-webkit-overflow-scrolling: touch;
touch-action: pan-x pan-y;
padding-bottom: var(--padding-small);
}
.tp__stats {
&__body {
padding-right: var(--padding-default);
padding-bottom: var(--padding-default);
&-group-mobile {
display: none;
width: 100%;
.tp__card {
width: 100%;
}
.tp__status-card {
flex-flow: row;
&__title {
flex-grow: 1;
}
}
}
@media (max-width: size.$mobile-breakpoint) {
&-group {
display: none;
}
&-group-mobile {
display: flex;
}
}
&-group-provider {
@media (max-width: size.$dashboard-mobile-breakpoint) {
display: flex;
flex-flow: row wrap;
}
}
}
}
}
.tp__stream-display {
display: flex;
flex-flow: column;
flex: 1 1 auto;
min-height: 0;
border: 1px solid var(--card-border-color);
border-radius: var(--border-radius);
background: var(--card-background-color);
box-shadow: var(--card-shadow);
&__header {
font-size: var(--font-size-md);
color: var(--modest-text-color);
padding: var(--padding-default) var(--padding-default) 0 var(--padding-default);
}
&__body {
display: flex;
flex: 1 1 auto;
min-height: 0;
padding: var(--padding-default);
overflow: hidden;
}
&__list {
display: flex;
flex: 1 1 auto;
flex-flow: column;
gap: var(--gap-small);
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
pointer-events: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
padding-right: var(--padding-small);
z-index: 1;
position: relative;
}
&__item {
display: flex;
flex-flow: row nowrap;
gap: var(--gap-small);
padding: var(--padding-default);
border: 1px solid var(--card-border-color);
border-radius: var(--border-radius);
background: var(--card-background-color);
pointer-events: auto;
&-content {
display: flex;
flex-flow: column;
gap: var(--gap-small);
width: 100%;
}
&-head {
display: flex;
flex-flow: row wrap;
gap: var(--gap-default);
justify-content: space-between;
align-items: flex-start;
}
}
&__identity {
display: flex;
flex: 1 1 auto;
flex-flow: column;
gap: var(--gap-small);
min-width: 0;
}
&__title-row {
display: flex;
gap: var(--gap-small);
align-items: flex-start;
}
&__menu {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
align-self: center;
width: 2rem;
height: 2rem;
border: 0;
border-radius: 50%;
background: transparent;
color: var(--text-color);
fill: var(--text-color);
cursor: pointer;
&:hover {
background: var(--menu-background-active-color);
color: var(--text-color);
}
}
&__title-block {
display: flex;
flex: 1 1 auto;
flex-flow: column;
gap: var(--gap-default);
min-width: 0;
margin-right: var(--margin-default);
}
&__title {
font-weight: 700;
line-height: 1.2;
min-width: 0;
overflow-wrap: anywhere;
}
&__title-line {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--gap-small);
min-width: 0;
}
&__recording-badge {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: var(--gap-mini);
padding: var(--padding-micro) var(--padding-mini);
border: 1px solid var(--attention-color);
border-radius: var(--tag-border-radius);
color: var(--attention-color);
background: var(--error-background-color);
font-size: var(--font-size-xs);
font-weight: 700;
line-height: 1.2;
white-space: nowrap;
&::before {
content: "";
width: 0.5em;
height: 0.5em;
border-radius: 50%;
background: currentColor;
}
}
&__subtitle {
display: flex;
flex-flow: row wrap;
gap: var(--gap-default);
min-width: 0;
overflow-wrap: anywhere;
}
&__username {
font-weight: bold;
color: var(--stream-tech-chip-video-codec-color);
}
&__user-comment {
color: var(--modest-text-color);
}
&__channel_type {
font-weight: bold;
color: var(--stream-tech-chip-audio-codec-color);
}
&__provider {
font-weight: bold;
color: var(--stream-tech-chip-container-color);
}
&__country {
display: inline-flex;
align-items: center;
gap: var(--gap-mini);
min-width: 0;
&--stat {
flex: 0 1 auto;
}
}
&__country-name,
&__country-code {
min-width: 0;
overflow-wrap: anywhere;
}
&__country-code {
color: var(--modest-text-color);
font-weight: 700;
}
&__flag {
display: inline-flex;
width: 1rem;
height: 0.75rem;
overflow: hidden;
border-radius: 2px;
svg {
display: block;
width: 100%;
height: 100%;
}
}
&__stats {
display: flex;
flex-flow: row wrap;
gap: var(--gap-default)
}
&__stat {
display: flex;
flex-flow: column;
gap: var(--gap-small);
padding: var(--padding-micro);
justify-content: center;
border-radius: var(--border-radius);
border: 1px solid var(--card-border-color);
}
&__stat--duration {
width: 90px;
}
&__stat-label {
color: var(--tertiary-title-text-color);
font-size: var(--font-size-xs);
font-weight: 700;
text-transform: uppercase;
}
&__stat-value {
font-weight: 600;
min-width: 0;
//.tp__toggle-switch {
// height: 1rem;
// max-height: 1rem;
//}
}
&__stat-value--ip {
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
line-height: 1.3;
}
&__duration {
letter-spacing: 0.12em;
}
&__epg-now {
color: var(--theme-brand-positive-strong);
}
&__epg-time {
font-size: 0.9rem;
}
&__epg-title {
font-weight: 700;
}
&__epg-separator {
font-size: 1.2rem;
}
&__epg-next {
color: var(--modest-text-color);
}
&__row {
display: flex;
flex-flow: row wrap;
gap: var(--gap-default);
justify-content: space-between;
}
&__chips {
display: flex;
flex-wrap: wrap;
gap: var(--gap-mini);
max-height: 1.2rem;
}
&__chip {
font-weight: 700;
padding: var(--padding-micro) var(--padding-small) !important;
&--container {
color: var(--stream-tech-chip-container-color) !important;
background-color: var(--stream-tech-chip-container-background-color) !important;
border-color: var(--stream-tech-chip-container-border-color) !important;
}
&--video-codec {
color: var(--stream-tech-chip-video-codec-color) !important;
background-color: var(--stream-tech-chip-video-codec-background-color) !important;
border-color: var(--stream-tech-chip-video-codec-border-color) !important;
}
&--resolution {
color: var(--stream-tech-chip-resolution-color) !important;
background-color: var(--stream-tech-chip-resolution-background-color) !important;
border-color: var(--stream-tech-chip-resolution-border-color) !important;
}
&--fps {
color: var(--stream-tech-chip-fps-color) !important;
background-color: var(--stream-tech-chip-fps-background-color) !important;
border-color: var(--stream-tech-chip-fps-border-color) !important;
}
&--audio-codec {
color: var(--stream-tech-chip-audio-codec-color) !important;
background-color: var(--stream-tech-chip-audio-codec-background-color) !important;
border-color: var(--stream-tech-chip-audio-codec-border-color) !important;
}
&--audio-channels {
color: var(--stream-tech-chip-audio-channels-color) !important;
background-color: var(--stream-tech-chip-audio-channels-background-color) !important;
border-color: var(--stream-tech-chip-audio-channels-border-color) !important;
}
}
@media (max-width: size.$dashboard-mobile-breakpoint) {
&__item-head {
flex-flow: column;
}
}
}