refactor(core): share title policy types
This commit is contained in:
@@ -3,8 +3,8 @@ use std::time::UNIX_EPOCH;
|
|||||||
|
|
||||||
use arr_core::policy::{evaluate, Candidate};
|
use arr_core::policy::{evaluate, Candidate};
|
||||||
use arr_core::{
|
use arr_core::{
|
||||||
DolbyVisionProfile, HdrRules, Language, MovieOverrides, Policy, PolicyId, RequiredAudio,
|
DolbyVisionProfile, HdrRules, Language, Policy, PolicyId, RequiredAudio, Resolution, Rule,
|
||||||
Resolution, Rule, ScoreWeights, SizeBand, Source, Verdict,
|
ScoreWeights, SizeBand, Source, TitleOverrides, Verdict,
|
||||||
};
|
};
|
||||||
use arr_indexer::{ProwlarrClient, SearchRelease, SearchRequest};
|
use arr_indexer::{ProwlarrClient, SearchRelease, SearchRequest};
|
||||||
use axum::extract::{Query, State};
|
use axum::extract::{Query, State};
|
||||||
@@ -293,7 +293,7 @@ pub async fn releases(
|
|||||||
})?;
|
})?;
|
||||||
let overrides: OverridesJson = serde_json::from_value(movie.overrides)
|
let overrides: OverridesJson = serde_json::from_value(movie.overrides)
|
||||||
.map_err(|error| ApiError::Database(error.to_string()))?;
|
.map_err(|error| ApiError::Database(error.to_string()))?;
|
||||||
let overrides = MovieOverrides {
|
let overrides = TitleOverrides {
|
||||||
only_4k: overrides.only_4k,
|
only_4k: overrides.only_4k,
|
||||||
allow_english_audio: overrides.allow_english_audio,
|
allow_english_audio: overrides.allow_english_audio,
|
||||||
};
|
};
|
||||||
@@ -460,7 +460,7 @@ fn policy_from_row(row: PolicyRow) -> Result<Policy, ApiError> {
|
|||||||
fn classify(
|
fn classify(
|
||||||
release: SearchRelease,
|
release: SearchRelease,
|
||||||
policy: &Policy,
|
policy: &Policy,
|
||||||
overrides: &MovieOverrides,
|
overrides: &TitleOverrides,
|
||||||
original_language: &Language,
|
original_language: &Language,
|
||||||
) -> Result<ClassifiedRelease, ApiError> {
|
) -> Result<ClassifiedRelease, ApiError> {
|
||||||
let parsed = arr_parse::parse(&release.name);
|
let parsed = arr_parse::parse(&release.name);
|
||||||
|
|||||||
@@ -163,10 +163,8 @@ pub struct TitleOverrides {
|
|||||||
pub allow_english_audio: bool,
|
pub allow_english_audio: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type MovieOverrides = TitleOverrides;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum MovieState {
|
pub enum MediaState {
|
||||||
Missing,
|
Missing,
|
||||||
Downloading,
|
Downloading,
|
||||||
Available,
|
Available,
|
||||||
@@ -181,8 +179,8 @@ pub struct Movie {
|
|||||||
pub original_language: Language,
|
pub original_language: Language,
|
||||||
pub root_id: RootId,
|
pub root_id: RootId,
|
||||||
pub wanted: bool,
|
pub wanted: bool,
|
||||||
pub overrides: MovieOverrides,
|
pub overrides: TitleOverrides,
|
||||||
pub state: MovieState,
|
pub state: MediaState,
|
||||||
pub blocked: bool,
|
pub blocked: bool,
|
||||||
pub search_attempts: u32,
|
pub search_attempts: u32,
|
||||||
pub last_searched_at: Option<SystemTime>,
|
pub last_searched_at: Option<SystemTime>,
|
||||||
@@ -210,13 +208,6 @@ pub struct Season {
|
|||||||
pub tracked: bool,
|
pub tracked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum EpisodeState {
|
|
||||||
Missing,
|
|
||||||
Downloading,
|
|
||||||
Available,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct Episode {
|
pub struct Episode {
|
||||||
pub id: EpisodeId,
|
pub id: EpisodeId,
|
||||||
@@ -225,7 +216,7 @@ pub struct Episode {
|
|||||||
pub title: String,
|
pub title: String,
|
||||||
pub air_date: Option<SystemTime>,
|
pub air_date: Option<SystemTime>,
|
||||||
pub wanted: bool,
|
pub wanted: bool,
|
||||||
pub state: EpisodeState,
|
pub state: MediaState,
|
||||||
pub search_attempts: u32,
|
pub search_attempts: u32,
|
||||||
pub last_searched_at: Option<SystemTime>,
|
pub last_searched_at: Option<SystemTime>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use arr_parse::{LanguageMarker, NameClaims};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
lang::{is_resolved_portuguese, language_of_marker},
|
lang::{is_resolved_portuguese, language_of_marker},
|
||||||
HdrFormat, Language, MovieOverrides, Policy, ProbedMedia, RequiredAudio, Resolution, Rule,
|
HdrFormat, Language, Policy, ProbedMedia, RequiredAudio, Resolution, Rule, Source,
|
||||||
Source, Verdict,
|
TitleOverrides, Verdict,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The evidence available while evaluating a candidate.
|
/// The evidence available while evaluating a candidate.
|
||||||
@@ -55,7 +55,7 @@ pub enum EvaluationPhase {
|
|||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct EvaluationContext<'a> {
|
pub struct EvaluationContext<'a> {
|
||||||
pub policy: &'a Policy,
|
pub policy: &'a Policy,
|
||||||
pub overrides: &'a MovieOverrides,
|
pub overrides: &'a TitleOverrides,
|
||||||
/// The title's original language from TMDB (`DESIGN.md` §5.2) — a
|
/// The title's original language from TMDB (`DESIGN.md` §5.2) — a
|
||||||
/// distinct input from any track's language.
|
/// distinct input from any track's language.
|
||||||
pub original_language: &'a Language,
|
pub original_language: &'a Language,
|
||||||
@@ -103,7 +103,7 @@ pub struct Evaluation {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn evaluate(
|
pub fn evaluate(
|
||||||
policy: &Policy,
|
policy: &Policy,
|
||||||
overrides: &MovieOverrides,
|
overrides: &TitleOverrides,
|
||||||
original_language: &Language,
|
original_language: &Language,
|
||||||
candidate: Candidate<'_>,
|
candidate: Candidate<'_>,
|
||||||
size_bytes: Option<u64>,
|
size_bytes: Option<u64>,
|
||||||
@@ -514,7 +514,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verdict(policy: &Policy, overrides: &MovieOverrides, candidate: Candidate<'_>) -> Verdict {
|
fn verdict(policy: &Policy, overrides: &TitleOverrides, candidate: Candidate<'_>) -> Verdict {
|
||||||
evaluate(policy, overrides, &en(), candidate, None).verdict
|
evaluate(policy, overrides, &en(), candidate, None).verdict
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,7 +525,7 @@ mod tests {
|
|||||||
) -> Verdict {
|
) -> Verdict {
|
||||||
evaluate(
|
evaluate(
|
||||||
policy,
|
policy,
|
||||||
&MovieOverrides::default(),
|
&TitleOverrides::default(),
|
||||||
original_language,
|
original_language,
|
||||||
candidate,
|
candidate,
|
||||||
None,
|
None,
|
||||||
@@ -539,7 +539,7 @@ mod tests {
|
|||||||
let claims = claims(None, None);
|
let claims = claims(None, None);
|
||||||
let report = evaluate(
|
let report = evaluate(
|
||||||
&policy,
|
&policy,
|
||||||
&MovieOverrides::default(),
|
&TitleOverrides::default(),
|
||||||
&en(),
|
&en(),
|
||||||
Candidate::PreGrab(&claims),
|
Candidate::PreGrab(&claims),
|
||||||
None,
|
None,
|
||||||
@@ -562,7 +562,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn preferred_resolutions_pass_in_both_phases() {
|
fn preferred_resolutions_pass_in_both_phases() {
|
||||||
let policy = policy();
|
let policy = policy();
|
||||||
let overrides = MovieOverrides::default();
|
let overrides = TitleOverrides::default();
|
||||||
|
|
||||||
for claimed in [ClaimedResolution::P2160, ClaimedResolution::P1080] {
|
for claimed in [ClaimedResolution::P2160, ClaimedResolution::P1080] {
|
||||||
let claims = claims(Some(claimed), Some(ClaimedSource::WebDl));
|
let claims = claims(Some(claimed), Some(ClaimedSource::WebDl));
|
||||||
@@ -583,7 +583,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn resolution_outside_root_policy_hard_fails_in_both_phases() {
|
fn resolution_outside_root_policy_hard_fails_in_both_phases() {
|
||||||
let policy = policy();
|
let policy = policy();
|
||||||
let overrides = MovieOverrides::default();
|
let overrides = TitleOverrides::default();
|
||||||
let cases = [
|
let cases = [
|
||||||
(ClaimedResolution::P480, Resolution::Other(480)),
|
(ClaimedResolution::P480, Resolution::Other(480)),
|
||||||
(ClaimedResolution::P576, Resolution::Other(576)),
|
(ClaimedResolution::P576, Resolution::Other(576)),
|
||||||
@@ -607,9 +607,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn only_4k_tightens_the_root_policy_in_both_phases() {
|
fn only_4k_tightens_the_root_policy_in_both_phases() {
|
||||||
let policy = policy();
|
let policy = policy();
|
||||||
let overrides = MovieOverrides {
|
let overrides = TitleOverrides {
|
||||||
only_4k: true,
|
only_4k: true,
|
||||||
..MovieOverrides::default()
|
..TitleOverrides::default()
|
||||||
};
|
};
|
||||||
let claims_1080 = claims(Some(ClaimedResolution::P1080), Some(ClaimedSource::WebDl));
|
let claims_1080 = claims(Some(ClaimedResolution::P1080), Some(ClaimedSource::WebDl));
|
||||||
let media = probed(Resolution::R1080p, Some(Source::WebDl));
|
let media = probed(Resolution::R1080p, Some(Source::WebDl));
|
||||||
@@ -634,9 +634,9 @@ mod tests {
|
|||||||
fn only_4k_never_expands_the_root_policy() {
|
fn only_4k_never_expands_the_root_policy() {
|
||||||
let mut policy = policy();
|
let mut policy = policy();
|
||||||
policy.resolution_preference = vec![Resolution::R1080p];
|
policy.resolution_preference = vec![Resolution::R1080p];
|
||||||
let overrides = MovieOverrides {
|
let overrides = TitleOverrides {
|
||||||
only_4k: true,
|
only_4k: true,
|
||||||
..MovieOverrides::default()
|
..TitleOverrides::default()
|
||||||
};
|
};
|
||||||
let claims = claims(Some(ClaimedResolution::P2160), Some(ClaimedSource::WebDl));
|
let claims = claims(Some(ClaimedResolution::P2160), Some(ClaimedSource::WebDl));
|
||||||
|
|
||||||
@@ -649,7 +649,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn every_unsafe_source_hard_fails_in_both_phases() {
|
fn every_unsafe_source_hard_fails_in_both_phases() {
|
||||||
let policy = policy();
|
let policy = policy();
|
||||||
let overrides = MovieOverrides::default();
|
let overrides = TitleOverrides::default();
|
||||||
let cases = [
|
let cases = [
|
||||||
(ClaimedSource::Cam, Source::Cam),
|
(ClaimedSource::Cam, Source::Cam),
|
||||||
(ClaimedSource::Telesync, Source::Telesync),
|
(ClaimedSource::Telesync, Source::Telesync),
|
||||||
@@ -679,7 +679,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
verdict(
|
verdict(
|
||||||
&policy,
|
&policy,
|
||||||
&MovieOverrides::default(),
|
&TitleOverrides::default(),
|
||||||
Candidate::PreGrab(&claims),
|
Candidate::PreGrab(&claims),
|
||||||
),
|
),
|
||||||
Verdict::Rejected(Rule::Source(Source::Telesync))
|
Verdict::Rejected(Rule::Source(Source::Telesync))
|
||||||
@@ -689,7 +689,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn every_safe_source_passes_in_both_phases() {
|
fn every_safe_source_passes_in_both_phases() {
|
||||||
let policy = policy();
|
let policy = policy();
|
||||||
let overrides = MovieOverrides::default();
|
let overrides = TitleOverrides::default();
|
||||||
let cases = [
|
let cases = [
|
||||||
(ClaimedSource::Dvd, Source::Dvd),
|
(ClaimedSource::Dvd, Source::Dvd),
|
||||||
(ClaimedSource::Telecine, Source::Telecine),
|
(ClaimedSource::Telecine, Source::Telecine),
|
||||||
@@ -721,7 +721,7 @@ mod tests {
|
|||||||
let media = probed(Resolution::R2160p, source);
|
let media = probed(Resolution::R2160p, source);
|
||||||
let report = evaluate(
|
let report = evaluate(
|
||||||
&policy,
|
&policy,
|
||||||
&MovieOverrides::default(),
|
&TitleOverrides::default(),
|
||||||
&en(),
|
&en(),
|
||||||
Candidate::PostDownload(&media),
|
Candidate::PostDownload(&media),
|
||||||
None,
|
None,
|
||||||
@@ -777,7 +777,7 @@ mod tests {
|
|||||||
|
|
||||||
let report = evaluate(
|
let report = evaluate(
|
||||||
policy,
|
policy,
|
||||||
&MovieOverrides::default(),
|
&TitleOverrides::default(),
|
||||||
&en(),
|
&en(),
|
||||||
Candidate::PostDownload(&media),
|
Candidate::PostDownload(&media),
|
||||||
None,
|
None,
|
||||||
@@ -796,7 +796,7 @@ mod tests {
|
|||||||
let claims = arr_parse::parse("Movie.2024.2160p.WEB-DL.DV-GROUP");
|
let claims = arr_parse::parse("Movie.2024.2160p.WEB-DL.DV-GROUP");
|
||||||
let report = evaluate(
|
let report = evaluate(
|
||||||
&policy(),
|
&policy(),
|
||||||
&MovieOverrides::default(),
|
&TitleOverrides::default(),
|
||||||
&en(),
|
&en(),
|
||||||
Candidate::PreGrab(&claims),
|
Candidate::PreGrab(&claims),
|
||||||
None,
|
None,
|
||||||
@@ -823,7 +823,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn hard_failure_wins_over_an_earlier_soft_failure() {
|
fn hard_failure_wins_over_an_earlier_soft_failure() {
|
||||||
let policy = policy();
|
let policy = policy();
|
||||||
let overrides = MovieOverrides::default();
|
let overrides = TitleOverrides::default();
|
||||||
let original = en();
|
let original = en();
|
||||||
let claims = claims(None, None);
|
let claims = claims(None, None);
|
||||||
let context = EvaluationContext {
|
let context = EvaluationContext {
|
||||||
@@ -851,7 +851,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn first_failure_of_the_deciding_kind_is_stable() {
|
fn first_failure_of_the_deciding_kind_is_stable() {
|
||||||
let policy = policy();
|
let policy = policy();
|
||||||
let overrides = MovieOverrides::default();
|
let overrides = TitleOverrides::default();
|
||||||
let original = en();
|
let original = en();
|
||||||
let claims = claims(None, None);
|
let claims = claims(None, None);
|
||||||
let context = EvaluationContext {
|
let context = EvaluationContext {
|
||||||
@@ -984,9 +984,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn kids_allow_english_audio_grabs_and_imports_with_a_waiver() {
|
fn kids_allow_english_audio_grabs_and_imports_with_a_waiver() {
|
||||||
let policy = kids_policy();
|
let policy = kids_policy();
|
||||||
let overrides = MovieOverrides {
|
let overrides = TitleOverrides {
|
||||||
allow_english_audio: true,
|
allow_english_audio: true,
|
||||||
..MovieOverrides::default()
|
..TitleOverrides::default()
|
||||||
};
|
};
|
||||||
let claims = name_claims(&[LanguageMarker::English]);
|
let claims = name_claims(&[LanguageMarker::English]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1078,7 +1078,7 @@ mod tests {
|
|||||||
// kids: could be the required pt-PT — must not reject as a dub.
|
// kids: could be the required pt-PT — must not reject as a dub.
|
||||||
let report = evaluate(
|
let report = evaluate(
|
||||||
&kids_policy(),
|
&kids_policy(),
|
||||||
&MovieOverrides::default(),
|
&TitleOverrides::default(),
|
||||||
&en(),
|
&en(),
|
||||||
Candidate::PostDownload(&media),
|
Candidate::PostDownload(&media),
|
||||||
None,
|
None,
|
||||||
@@ -1089,7 +1089,7 @@ mod tests {
|
|||||||
// as verified either.
|
// as verified either.
|
||||||
let report = evaluate(
|
let report = evaluate(
|
||||||
&policy(),
|
&policy(),
|
||||||
&MovieOverrides::default(),
|
&TitleOverrides::default(),
|
||||||
&Language::PortugueseBrazil,
|
&Language::PortugueseBrazil,
|
||||||
Candidate::PostDownload(&media),
|
Candidate::PostDownload(&media),
|
||||||
None,
|
None,
|
||||||
@@ -1102,7 +1102,7 @@ mod tests {
|
|||||||
let claims = name_claims(&[LanguageMarker::Portuguese]);
|
let claims = name_claims(&[LanguageMarker::Portuguese]);
|
||||||
let report = evaluate(
|
let report = evaluate(
|
||||||
&kids_policy(),
|
&kids_policy(),
|
||||||
&MovieOverrides::default(),
|
&TitleOverrides::default(),
|
||||||
&en(),
|
&en(),
|
||||||
Candidate::PreGrab(&claims),
|
Candidate::PreGrab(&claims),
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
policy::{evaluate, EvaluationContext, PolicyRule, RuleEvaluation, SizeRule},
|
policy::{evaluate, EvaluationContext, PolicyRule, RuleEvaluation, SizeRule},
|
||||||
HdrRules, Language, MovieOverrides, PolicyId, RequiredAudio, Rule, Verdict,
|
HdrRules, Language, PolicyId, RequiredAudio, Rule, TitleOverrides, Verdict,
|
||||||
};
|
};
|
||||||
|
|
||||||
const GIB: u64 = 1 << 30;
|
const GIB: u64 = 1 << 30;
|
||||||
@@ -221,7 +221,7 @@ mod tests {
|
|||||||
|
|
||||||
fn size_rule(size_bytes: u64) -> RuleEvaluation {
|
fn size_rule(size_bytes: u64) -> RuleEvaluation {
|
||||||
let policy = policy();
|
let policy = policy();
|
||||||
let overrides = MovieOverrides::default();
|
let overrides = TitleOverrides::default();
|
||||||
let language = Language::Other("en".to_owned());
|
let language = Language::Other("en".to_owned());
|
||||||
let claims = claims(ClaimedSource::WebDl);
|
let claims = claims(ClaimedSource::WebDl);
|
||||||
SizeRule.evaluate(&EvaluationContext {
|
SizeRule.evaluate(&EvaluationContext {
|
||||||
@@ -256,7 +256,7 @@ mod tests {
|
|||||||
let claims = claims(ClaimedSource::Remux);
|
let claims = claims(ClaimedSource::Remux);
|
||||||
let evaluation = evaluate(
|
let evaluation = evaluate(
|
||||||
&policy,
|
&policy,
|
||||||
&MovieOverrides::default(),
|
&TitleOverrides::default(),
|
||||||
&Language::Other("en".to_owned()),
|
&Language::Other("en".to_owned()),
|
||||||
Candidate::PreGrab(&claims),
|
Candidate::PreGrab(&claims),
|
||||||
Some(gib(60)),
|
Some(gib(60)),
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ CREATE TABLE episodes (
|
|||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
air_date TEXT,
|
air_date TEXT,
|
||||||
wanted INTEGER NOT NULL DEFAULT 0 CHECK (wanted IN (0, 1)),
|
wanted INTEGER NOT NULL DEFAULT 0 CHECK (wanted IN (0, 1)),
|
||||||
-- Mirrors arr-core's canonical EpisodeState vocabulary.
|
-- Mirrors arr-core's canonical MediaState vocabulary. The older movies
|
||||||
|
-- table uses legacy names; its additive correction is tracked in #73.
|
||||||
state TEXT NOT NULL DEFAULT 'missing'
|
state TEXT NOT NULL DEFAULT 'missing'
|
||||||
CHECK (state IN ('missing', 'downloading', 'available')),
|
CHECK (state IN ('missing', 'downloading', 'available')),
|
||||||
search_attempts INTEGER NOT NULL DEFAULT 0,
|
search_attempts INTEGER NOT NULL DEFAULT 0,
|
||||||
@@ -91,6 +92,7 @@ INSERT INTO policies (
|
|||||||
json('{"dv_profile_allow":["8.1"],"dv_profile_reject":["5","7"],"allow_hdr10":true,"allow_hdr10plus":true,"allow_sdr":true}'),
|
json('{"dv_profile_allow":["8.1"],"dv_profile_reject":["5","7"],"allow_hdr10":true,"allow_hdr10plus":true,"allow_sdr":true}'),
|
||||||
json('{"2160p":{"floor_gib":8,"target_gib":22,"penalty_points_per_gib_over":60},"1080p":{"floor_gib":3,"target_gib":8,"penalty_points_per_gib_over":60}}'),
|
json('{"2160p":{"floor_gib":8,"target_gib":22,"penalty_points_per_gib_over":60},"1080p":{"floor_gib":3,"target_gib":8,"penalty_points_per_gib_over":60}}'),
|
||||||
json('["2160p","1080p"]'),
|
json('["2160p","1080p"]'),
|
||||||
|
-- §5.5 deliberately keeps source tier as Remux > BluRay > WEB-DL.
|
||||||
json('{"Remux":4,"BluRay":3,"WEB-DL":2,"WEBRip":1,"HDTV":0}'),
|
json('{"Remux":4,"BluRay":3,"WEB-DL":2,"WEBRip":1,"HDTV":0}'),
|
||||||
json('{"size_at_target":1000,"source_tier":25,"seeder_doubling":8}')
|
json('{"size_at_target":1000,"source_tier":25,"seeder_doubling":8}')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ mod tests {
|
|||||||
.bind(season_id)
|
.bind(season_id)
|
||||||
.execute(db.pool())
|
.execute(db.pool())
|
||||||
.await
|
.await
|
||||||
.expect_err("database mirrors EpisodeState");
|
.expect_err("database mirrors MediaState");
|
||||||
sqlx::query("UPDATE series SET upstream_ended = 2 WHERE id = ?")
|
sqlx::query("UPDATE series SET upstream_ended = 2 WHERE id = ?")
|
||||||
.bind(series_id)
|
.bind(series_id)
|
||||||
.execute(db.pool())
|
.execute(db.pool())
|
||||||
|
|||||||
Reference in New Issue
Block a user