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 @@
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
-
-
+
@@ -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
-
+
Add source
-
+
Refresh all
-
+
Clear history
-
+
Delete everything
@@ -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