mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-25 08:12:19 +01:00
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.
412 lines
8.4 KiB
SCSS
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;
|
|
}
|
|
}
|
|
}
|