about summary refs log tree commit diff stats
path: root/src/storage/video_database/downloader.rs
blob: c2c148dec838129c599ec4de723e8d6db4241f4d (plain) (blame)
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// yt - A fully featured command line YouTube client
//
// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of Yt.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.

use std::path::{Path, PathBuf};

use anyhow::Result;
use log::debug;
use sqlx::query;
use url::Url;

use crate::{app::App, storage::video_database::VideoStatus};

use super::{ExtractorHash, Video};

/// Returns to next video which should be downloaded. This respects the priority assigned by select.
/// It does not return videos, which are already cached.
pub async fn get_next_uncached_video(app: &App) -> Result<Option<Video>> {
    let status = VideoStatus::Watch.as_db_integer();

    // NOTE: The ORDER BY statement should be the same as the one in [`getters::get_videos`].<2024-08-22>
    let result = query!(
        r#"
        SELECT *
        FROM videos
        WHERE status = ? AND cache_path IS NULL
        ORDER BY priority DESC, publish_date DESC
        LIMIT 1;
    "#,
        status
    )
    .fetch_one(&app.database)
    .await;

    if let Err(sqlx::Error::RowNotFound) = result {
        Ok(None)
    } else {
        let base = result?;

        let thumbnail_url = if let Some(url) = &base.thumbnail_url {
            Some(Url::parse(&url).expect("Parsing this as url should always work"))
        } else {
            None
        };

        let status_change = if base.status_change == 1 {
            true
        } else {
            assert_eq!(base.status_change, 0, "Can only be 1 or 0");
            false
        };

        let video = Video {
            cache_path: base.cache_path.as_ref().map(|val| PathBuf::from(val)),
            description: base.description.clone(),
            duration: base.duration,
            extractor_hash: ExtractorHash::from_hash(
                base.extractor_hash
                    .parse()
                    .expect("The hash in the db should be valid"),
            ),
            last_status_change: base.last_status_change,
            parent_subscription_name: base.parent_subscription_name.clone(),
            priority: base.priority,
            publish_date: base.publish_date,
            status: VideoStatus::from_db_integer(base.status),
            status_change,
            thumbnail_url,
            title: base.title.clone(),
            url: Url::parse(&base.url).expect("Parsing this as url should always work"),
        };

        Ok(Some(video))
    }
}

/// Returns to next video which can be watched (i.e. is cached).
/// This respects the priority assigned by select.
pub async fn get_next_video_watchable(app: &App) -> Result<Option<Video>> {
    let result = query!(
        r#"
        SELECT *
        FROM videos
        WHERE status = 'Watching' AND cache_path IS NOT NULL
        ORDER BY priority ASC
        LIMIT 1;
    "#
    )
    .fetch_one(&app.database)
    .await;

    if let Err(sqlx::Error::RowNotFound) = result {
        Ok(None)
    } else {
        let base = result?;

        let thumbnail_url = if let Some(url) = &base.thumbnail_url {
            Some(Url::parse(&url).expect("Parsing this as url should always work"))
        } else {
            None
        };

        let status_change = if base.status_change == 1 {
            true
        } else {
            assert_eq!(base.status_change, 0, "Can only be 1 or 0");
            false
        };

        let video = Video {
            cache_path: base.cache_path.as_ref().map(|val| PathBuf::from(val)),
            description: base.description.clone(),
            duration: base.duration,
            extractor_hash: ExtractorHash::from_hash(
                base.extractor_hash
                    .parse()
                    .expect("The db extractor_hash should be valid blake3 hash"),
            ),
            last_status_change: base.last_status_change,
            parent_subscription_name: base.parent_subscription_name.clone(),
            priority: base.priority,
            publish_date: base.publish_date,
            status: VideoStatus::from_db_integer(base.status),
            status_change,
            thumbnail_url,
            title: base.title.clone(),
            url: Url::parse(&base.url).expect("Parsing this as url should always work"),
        };

        Ok(Some(video))
    }
}

/// Update the cached path of a video. Will be set to NULL if the path is None
/// This will also set the status to `Cached` when path is Some, otherwise it set's the status to
/// `Watch`.
pub async fn set_video_cache_path(
    app: &App,
    video: &ExtractorHash,
    path: Option<&Path>,
) -> Result<()> {
    if let Some(path) = path {
        debug!(
            "Setting cache path from '{}' to '{}'",
            video.into_short_hash(app).await?,
            path.display()
        );

        let path_str = path.display().to_string();
        let extractor_hash = video.hash().to_string();
        let status = VideoStatus::Cached.as_db_integer();

        query!(
            r#"
            UPDATE videos
            SET cache_path = ?, status = ?
            WHERE extractor_hash = ?;
        "#,
            path_str,
            status,
            extractor_hash
        )
        .execute(&app.database)
        .await?;

        Ok(())
    } else {
        debug!(
            "Setting cache path from '{}' to NULL",
            video.into_short_hash(app).await?,
        );

        let extractor_hash = video.hash().to_string();
        let status = VideoStatus::Watch.as_db_integer();

        query!(
            r#"
            UPDATE videos
            SET cache_path = NULL, status = ?
            WHERE extractor_hash = ?;
        "#,
            status,
            extractor_hash
        )
        .execute(&app.database)
        .await?;

        Ok(())
    }
}

/// Returns the number of cached videos
pub async fn get_allocated_cache(app: &App) -> Result<u32> {
    let count = query!(
        r#"
        SELECT COUNT(cache_path) as count
        FROM videos
        WHERE cache_path IS NOT NULL;
"#,
    )
    .fetch_one(&app.database)
    .await?;

    Ok(count.count as u32)
}