aboutsummaryrefslogtreecommitdiff
path: root/logic/src/home.rs
blob: 1957a94abc83c474ed0a708eaf2cfe23ae8bd74c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/*
    This file is part of jellything (https://codeberg.org/metamuffin/jellything)
    which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
    Copyright (C) 2025 metamuffin <metamuffin.org>
*/

use crate::{DATABASE, node::DatabaseNodeUserDataExt, session::Session};
use anyhow::{Context, Result};
use jellycommon::{
    NodeID, NodeKind, Rating, Visibility,
    api::ApiHomeResponse,
    chrono::{Datelike, Utc},
    user::WatchedState,
};

pub fn home(session: &Session) -> Result<ApiHomeResponse> {
    let mut items = DATABASE.list_nodes_with_udata(&session.user.name)?;

    let mut toplevel = DATABASE
        .get_node_children(NodeID::from_slug("library"))
        .context("root node missing")?
        .into_iter()
        .map(|n| DATABASE.get_node_with_userdata(n, &session))
        .collect::<anyhow::Result<Vec<_>>>()?;
    toplevel.sort_by_key(|(n, _)| n.index.unwrap_or(usize::MAX));

    let mut categories = Vec::<(String, Vec<_>)>::new();

    categories.push((
        "home.bin.continue_watching".to_string(),
        items
            .iter()
            .filter(|(_, u)| matches!(u.watched, WatchedState::Progress(_)))
            .cloned()
            .collect(),
    ));
    categories.push((
        "home.bin.watchlist".to_string(),
        items
            .iter()
            .filter(|(_, u)| matches!(u.watched, WatchedState::Pending))
            .cloned()
            .collect(),
    ));

    items.retain(|(n, _)| matches!(n.visibility, Visibility::Visible));

    items.sort_by_key(|(n, _)| n.release_date.map(|d| -d).unwrap_or(i64::MAX));

    categories.push((
        "home.bin.latest_video".to_string(),
        items
            .iter()
            .filter(|(n, _)| matches!(n.kind, NodeKind::Video))
            .take(16)
            .cloned()
            .collect(),
    ));
    categories.push((
        "home.bin.latest_music".to_string(),
        items
            .iter()
            .filter(|(n, _)| matches!(n.kind, NodeKind::Music))
            .take(16)
            .cloned()
            .collect(),
    ));
    categories.push((
        "home.bin.latest_short_form".to_string(),
        items
            .iter()
            .filter(|(n, _)| matches!(n.kind, NodeKind::ShortFormVideo))
            .take(16)
            .cloned()
            .collect(),
    ));

    items.sort_by_key(|(n, _)| {
        n.ratings
            .get(&Rating::Tmdb)
            .map(|x| (*x * -1000.) as i32)
            .unwrap_or(0)
    });

    categories.push((
        "home.bin.max_rating".to_string(),
        items
            .iter()
            .take(16)
            .filter(|(n, _)| n.ratings.contains_key(&Rating::Tmdb))
            .cloned()
            .collect(),
    ));

    items.retain(|(n, _)| {
        matches!(
            n.kind,
            NodeKind::Video | NodeKind::Movie | NodeKind::Episode | NodeKind::Music
        )
    });

    categories.push((
        "home.bin.daily_random".to_string(),
        (0..16)
            .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone()))
            .collect(),
    ));

    {
        let mut items = items.clone();
        items.retain(|(_, u)| matches!(u.watched, WatchedState::Watched));
        categories.push((
            "home.bin.watch_again".to_string(),
            (0..16)
                .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone()))
                .collect(),
        ));
    }

    items.retain(|(n, _)| matches!(n.kind, NodeKind::Music));
    categories.push((
        "home.bin.daily_random_music".to_string(),
        (0..16)
            .flat_map(|i| Some(items[cheap_daily_random(i).checked_rem(items.len())?].clone()))
            .collect(),
    ));

    Ok(ApiHomeResponse {
        toplevel,
        categories,
    })
}

fn cheap_daily_random(i: usize) -> usize {
    xorshift(xorshift(Utc::now().num_days_from_ce() as u64) + i as u64) as usize
}

fn xorshift(mut x: u64) -> u64 {
    x ^= x << 13;
    x ^= x >> 7;
    x ^= x << 17;
    x
}