diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 13357704..1a54f218 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1202,11 +1202,26 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1214,15 +1229,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1231,9 +1246,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1250,9 +1265,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1261,22 +1276,23 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1284,7 +1300,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2971,6 +2986,7 @@ dependencies = [ "bytes", "chrono", "directories", + "futures", "if-addrs", "indexmap 2.12.1", "log", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 354f683f..99be5ad8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -45,6 +45,7 @@ warp = "0.3.7" if-addrs = "0.13.4" tokio-util = "0.7.17" indexmap = "2.12.1" +futures = "0.3.32" [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] shell-words = "1.1.0" [target.'cfg(target_os = "windows")'.dependencies] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 95c90bf1..c959262b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + #[cfg(any(target_os = "macos", target_os = "windows"))] use anyhow::Context; use anyhow::Error; @@ -113,6 +115,7 @@ pub fn run() { hide_channel, hide_group, remove_from_history, + get_all_expiries ]) .setup(|app| { app.manage(Mutex::new(AppState { @@ -561,3 +564,8 @@ async fn cancel_play( .await .map_err(map_err_frontend) } + +#[tauri::command] +async fn get_all_expiries() -> Result, String> { + xtream::get_all_expiries().await.map_err(map_err_frontend) +} diff --git a/src-tauri/src/sql.rs b/src-tauri/src/sql.rs index f35e601c..9720622d 100644 --- a/src-tauri/src/sql.rs +++ b/src-tauri/src/sql.rs @@ -1,3 +1,4 @@ +use std::vec; use std::{collections::HashMap, sync::LazyLock}; use crate::log::log; @@ -1174,6 +1175,16 @@ pub fn get_sources() -> Result> { Ok(sources) } +pub fn get_sources_by_type(source_type: u8) -> Result> { + let sql = get_conn()?; + let sources: Vec = sql + .prepare("SELECT * FROM sources WHERE source_type = ?")? + .query_map([source_type], row_to_source)? + .filter_map(Result::ok) + .collect(); + Ok(sources) +} + pub fn get_enabled_sources() -> Result> { let sql = get_conn()?; let sources: Vec = sql diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 83c306c1..3a1e03a3 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -68,6 +68,16 @@ pub struct Source { pub last_updated: Option, } +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct XtreamStatus { + pub user_info: XtreamStatusUserInfo, +} + +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct XtreamStatusUserInfo { + pub exp_date: serde_json::Value, +} + #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] pub struct Settings { pub recording_path: Option, diff --git a/src-tauri/src/xtream.rs b/src-tauri/src/xtream.rs index d3f083ed..6bad1e3f 100644 --- a/src-tauri/src/xtream.rs +++ b/src-tauri/src/xtream.rs @@ -1,5 +1,6 @@ use crate::log; use crate::media_type; +use crate::source_type; use crate::sql; use crate::sql::insert_season; use crate::types::Channel; @@ -7,6 +8,7 @@ use crate::types::ChannelPreserve; use crate::types::EPG; use crate::types::Season; use crate::types::Source; +use crate::types::XtreamStatus; use crate::utils::get_local_time; use crate::utils::get_user_agent_from_source; use anyhow::anyhow; @@ -16,6 +18,7 @@ use base64::prelude::BASE64_STANDARD; use chrono::DateTime; use chrono::Local; use chrono::NaiveDateTime; +use futures::future::join_all; use reqwest::Client; use rusqlite::Transaction; use serde::Deserialize; @@ -588,3 +591,27 @@ fn get_timeshift_url(mut url: Url, start: String, end: String, stream_id: &str) .append_pair("duration", &duration); Ok(url.to_string()) } + +async fn get_status(source: &mut Source) -> Result<(i64, XtreamStatus)> { + let url = build_xtream_url(source)?; + let user_agent = get_user_agent_from_source(&source)?; + let client = Client::builder().user_agent(user_agent).build()?; + let data = client.get(url).send().await?.json::().await?; + Ok((source.id.context("no id")?, data)) +} + +pub async fn get_all_expiries() -> Result> { + let mut sources = sql::get_sources_by_type(source_type::XTREAM)?; + let to_await = sources.iter_mut().map(|source| get_status(source)); + let results: Vec> = + join_all(to_await).await; + let statuses: HashMap = results + .into_iter() + .flatten() + .filter_map(|(id, status)| { + let exp_date = get_serde_json_i64(&status.user_info.exp_date)?; + Some((id, exp_date)) + }) + .collect(); + Ok(statuses) +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2da694da..cc2e5bd0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -33,11 +33,13 @@ import { SortItemComponent } from './home/sort-button/sort-item/sort-item.compon import { DownloadManagerComponent } from './download-manager/download-manager.component'; import { TimeAgoPipe } from "./pipes/time-ago.pipe"; +import { TimeUntilPipe } from './pipes/time-until.pipe'; @NgModule({ declarations: [ AppComponent, TimeAgoPipe, + TimeUntilPipe, SetupComponent, LoadingComponent, SourceNameExistsValidator, diff --git a/src/app/pipes/time-until.pipe.ts b/src/app/pipes/time-until.pipe.ts new file mode 100644 index 00000000..6635f254 --- /dev/null +++ b/src/app/pipes/time-until.pipe.ts @@ -0,0 +1,72 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'timeUntil' +}) +export class TimeUntilPipe implements PipeTransform { + + transform(value: any): string { + if (!value) return ''; + + const targetDate = new Date(value); + const now = new Date(); + const seconds = Math.floor((targetDate.getTime() - now.getTime()) / 1000); + const formattedDate = this.formatExactDate(targetDate); + + if (seconds < 0) + return `Expired (${formattedDate})`; + + const intervals: { [key: string]: number } = { + 'year': 31536000, + 'month': 2592000, + 'week': 604800, + 'day': 86400, + 'hour': 3600 + }; + + let counter; + let relativeString = ''; + + for (const i in intervals) { + counter = Math.floor(seconds / intervals[i]); + if (counter > 0) { + if (counter === 1) { + relativeString = `In ${counter} ${i}`; + } else { + relativeString = `In ${counter} ${i}s`; + } + break; + } + } + + if (!relativeString) { + relativeString = 'In less than an hour'; + } + + return `${relativeString} (${formattedDate})`; + } + + private formatExactDate(date: Date): string { + const monthNames = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + + const month = monthNames[date.getMonth()]; + const day = date.getDate(); + const year = date.getFullYear(); + const suffix = this.getOrdinalSuffix(day); + + return `${month} ${day}${suffix} ${year}`; + } + + private getOrdinalSuffix(day: number): string { + if (day > 3 && day < 21) return 'th'; + switch (day % 10) { + case 1: return "st"; + case 2: return "nd"; + case 3: return "rd"; + default: return "th"; + } + } +} \ No newline at end of file diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index 3a06e2bd..068407a4 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -1,7 +1,6 @@ + d="M2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12M18,11H10L13.5,7.5L12.08,6.08L6.16,12L12.08,17.92L13.5,16.5L10,13H18V11Z" />

Settings

@@ -11,21 +10,15 @@

Settings

- ~/Home/Videos + ~/Home/Videos {{ settings.recording_path - }} -
@@ -34,21 +27,14 @@

Settings

- Global MPV Parameters + ">Global MPV Parameters
- + class="form-control">
@@ -57,11 +43,7 @@

Settings

Default view
- @@ -74,11 +56,7 @@

Settings

Default sort
- @@ -91,16 +69,8 @@

Settings

Volume on start
- + {{ settings.volume }}
@@ -110,12 +80,7 @@

Settings

Re-stream port
- +
@@ -125,14 +90,7 @@

Settings

- + %
@@ -140,84 +98,56 @@

Settings

- Use stream caching + ">Use stream caching
- +
- Enable hardware acceleration + ">Enable hardware acceleration
- +
- Use modern hardware player optimizations (HDR, gpu playback, etc) + ">Use modern hardware player optimizations (HDR, gpu playback, etc)
- +
- Always ask where to save downloads + ">Always ask where to save downloads
- +
@@ -228,30 +158,20 @@

Settings

- +
- Enable tray icon + Enable tray icon
- +
@@ -259,59 +179,40 @@

Settings

Sources

- +
- - - -
@@ -325,4 +226,4 @@

Sources

sustainable and needs your support

- + \ No newline at end of file diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 50c15e1f..24221406 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -32,6 +32,7 @@ export class SettingsComponent { }; viewModeEnum = ViewMode; sources: Source[] = []; + expiries: Record = {}; sortTypes = SORT_TYPES; @ViewChild("mpvParams") mpvParams!: ElementRef; @@ -40,7 +41,7 @@ export class SettingsComponent { public memory: MemoryService, private nav: Router, private modal: NgbModal, - ) {} + ) { } _getSortTypeText(sortType: SortType) { return getSortTypeText(sortType); @@ -74,6 +75,8 @@ export class SettingsComponent { ngOnInit(): void { this.getSettings(); this.getSources(); + if (this.memory.XtreamSourceIds.size > 0) + this.getExpiries(); } getSettings() { @@ -102,6 +105,12 @@ export class SettingsComponent { }); } + getExpiries() { + invoke("get_all_expiries").then(expiries => { + this.expiries = expiries as Record; + }); + } + ngAfterViewInit(): void { this.subscriptions.push( fromEvent(this.mpvParams.nativeElement, "keyup") diff --git a/src/app/settings/source-tile/source-tile.component.html b/src/app/settings/source-tile/source-tile.component.html index c555c321..bb3d9160 100644 --- a/src/app/settings/source-tile/source-tile.component.html +++ b/src/app/settings/source-tile/source-tile.component.html @@ -105,6 +105,12 @@

{{source?.name}}

+
+
Expires:
+
+
{{expiry ? (expiry! * 1000 | timeUntil) : null}}
+
+
diff --git a/src/app/settings/source-tile/source-tile.component.ts b/src/app/settings/source-tile/source-tile.component.ts index 57c67f5a..eb37b0b6 100644 --- a/src/app/settings/source-tile/source-tile.component.ts +++ b/src/app/settings/source-tile/source-tile.component.ts @@ -19,6 +19,8 @@ import { sanitizeFileName } from "../../utils"; export class SourceTileComponent { @Input("source") source?: Source; + @Input("expiry") + expiry?: number; showUsername = false; showPassword = false; loading = false; @@ -30,7 +32,7 @@ export class SourceTileComponent { constructor( public memory: MemoryService, private modal: NgbModal, - ) {} + ) { } get_source_type_name() { if (!this.source) return null; diff --git a/src/styles.css b/src/styles.css index cebf2011..718063fe 100644 --- a/src/styles.css +++ b/src/styles.css @@ -32,7 +32,7 @@ body { } .selectable { - user-select: auto; + user-select: text; } p { @@ -53,7 +53,8 @@ body { height: fit-content; user-select: none; } + body { margin: 0; font-family: "Roboto", sans-serif; -} +} \ No newline at end of file