added cargo files
4
PinePods-0.8.2/web/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
keystore.properties
|
||||
4918
PinePods-0.8.2/web/src-tauri/Cargo.lock
generated
Normal file
37
PinePods-0.8.2/web/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "Pinepods-tauri"
|
||||
authors = ["Gooseberry Development"]
|
||||
license = ""
|
||||
repository = ""
|
||||
default-run = "app"
|
||||
edition = "2021"
|
||||
rust-version = "1.89"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.5.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0.145"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
tauri = { version = "2.9.2", features = ["tray-icon"] }
|
||||
directories = "6.0.0"
|
||||
dirs = "6.0.0"
|
||||
# reqwest = { version = "0.12.5", features = ["blocking", "json"] }
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
warp = { version = "0.4.2", features = ["server"] }
|
||||
ureq = "=3.1.2"
|
||||
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
3
PinePods-0.8.2/web/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
22
PinePods-0.8.2/web/src-tauri/change-version.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
def update_version(file_path, new_version):
|
||||
with open(file_path, 'r') as file:
|
||||
config = json.load(file)
|
||||
|
||||
# Update the version at the root level
|
||||
config['version'] = new_version
|
||||
|
||||
with open(file_path, 'w') as file:
|
||||
json.dump(config, file, indent=2)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: update_version.py <file_path> <new_version>")
|
||||
sys.exit(1)
|
||||
|
||||
file_path = sys.argv[1]
|
||||
new_version = sys.argv[2]
|
||||
|
||||
update_version(file_path, new_version)
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>com.gooseberrydevelopment.pinepods</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0-only</project_license>
|
||||
|
||||
<name>Pinepods</name>
|
||||
<summary>A complete, self-hosted podcast management system</summary>
|
||||
|
||||
<developer id="com.gooseberrydevelopment">
|
||||
<name>Gooseberry Development</name>
|
||||
</developer>
|
||||
|
||||
<description>
|
||||
<p>
|
||||
PinePods is a Rust based podcast management system that manages podcasts with multi-user support and relies on a central database with clients to connect to it. It's browser based and your podcasts and settings follow you from device to device due to everything being stored on the server. You can subscribe to podcasts and even hosts for podcasts with the help of the PodPeopleDB. It works on mobile devices and can also sync with a Nextcloud server or gpodder compatible sync server so you can use external apps like Antennapod as well! NOTE: This is the client edition of Pinepods. You must have a server to connect to in order to use this app.
|
||||
</p>
|
||||
<p>
|
||||
Instructions for setting up your own self-hosted server can be found on the main repo
|
||||
</p>
|
||||
</description>
|
||||
<launchable type="desktop-id">com.gooseberrydevelopment.pinepods.desktop</launchable>
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://raw.githubusercontent.com/madeofpendletonwool/PinePods/3c84f982bc3d320e2d4ebe6ce2788f5fd656f7b5/images/screenshots/homethemed.png</image>
|
||||
<caption>Main interface of Pinepods</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<url type="homepage">https://github.com/madeofpendletonwool/PinePods</url>
|
||||
<url type="bugtracker">https://github.com/madeofpendletonwool/PinePods/issues</url>
|
||||
<url type="donation">https://github.com/sponsors/madeofpendletonwool</url>
|
||||
|
||||
<releases>
|
||||
<release version="0.7.6" date="2025-03-23">
|
||||
<description>
|
||||
<p>I'm excited to announce PinePods 0.7.6, fully showing the power of the new notification system, things like episode downloads will now display percentages as they download in a brand new alert component that's been added. A worker system for background tasks that can be easily monitored on a per user basis so we can now get status of tasks as they execute was added back in version 0.7.4. Now we're starting to see what can be done with it. At this point I am confident Pinepods is the ultimate podcast archival tool. In the near future I will continue to migrate tasks over to this new system which will allow for even greater visibility into exactly what the server is doing. Think monitoring status of Nextcloud sync tasks etc.
|
||||
|
||||
In addition this update further improves some findings after the previous 0.7.4 update as it was a big one!</p>
|
||||
</description>
|
||||
</release>
|
||||
</releases>
|
||||
<requires>
|
||||
<display_length compare="ge">360</display_length>
|
||||
</requires>
|
||||
<recommends>
|
||||
<display_length compare="ge">548</display_length>
|
||||
</recommends>
|
||||
|
||||
<content_rating type="oars-1.1" />
|
||||
</component>
|
||||
1756
PinePods-0.8.2/web/src-tauri/gen/schemas/android-schema.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
2244
PinePods-0.8.2/web/src-tauri/gen/schemas/desktop-schema.json
Normal file
2244
PinePods-0.8.2/web/src-tauri/gen/schemas/linux-schema.json
Normal file
1756
PinePods-0.8.2/web/src-tauri/gen/schemas/mobile-schema.json
Normal file
BIN
PinePods-0.8.2/web/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/Square1024x1024.png
Normal file
|
After Width: | Height: | Size: 355 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/Square512x512.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/icon.icns
Normal file
BIN
PinePods-0.8.2/web/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
PinePods-0.8.2/web/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
563
PinePods-0.8.2/web/src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,563 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use directories::ProjectDirs;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::copy;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tauri::command;
|
||||
|
||||
fn deserialize_categories<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde::de::{self, Visitor};
|
||||
use std::fmt;
|
||||
|
||||
struct CategoriesVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for CategoriesVisitor {
|
||||
type Value = HashMap<String, String>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string or a map")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
// Convert comma-separated string to HashMap
|
||||
let mut map = HashMap::new();
|
||||
if !value.is_empty() && value != "{}" {
|
||||
for (i, category) in value.split(',').enumerate() {
|
||||
map.insert(i.to_string(), category.trim().to_string());
|
||||
}
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: de::MapAccess<'de>,
|
||||
{
|
||||
let mut categories = HashMap::new();
|
||||
while let Some((key, value)) = map.next_entry()? {
|
||||
categories.insert(key, value);
|
||||
}
|
||||
Ok(categories)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(CategoriesVisitor)
|
||||
}
|
||||
|
||||
// Define the structure for the file entries
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct FileEntry {
|
||||
path: String,
|
||||
}
|
||||
|
||||
// Function to list directory contents
|
||||
#[command]
|
||||
async fn list_dir(path: String) -> Result<Vec<FileEntry>, String> {
|
||||
let home_dir = dirs::home_dir().ok_or("Cannot find home directory")?;
|
||||
let target_path = if path == "~" {
|
||||
home_dir
|
||||
} else {
|
||||
PathBuf::from(path)
|
||||
};
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for entry in fs::read_dir(target_path).map_err(|e| e.to_string())? {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
entries.push(FileEntry {
|
||||
path: entry.path().display().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn get_project_dirs() -> Result<ProjectDirs, String> {
|
||||
ProjectDirs::from("com", "gooseberrydevelopment", "pinepods")
|
||||
.ok_or_else(|| "Cannot determine project directories".to_string())
|
||||
}
|
||||
|
||||
#[command]
|
||||
fn get_app_dir() -> Result<String, String> {
|
||||
let proj_dirs = get_project_dirs()?;
|
||||
let app_dir = proj_dirs.data_dir();
|
||||
if !app_dir.exists() {
|
||||
fs::create_dir_all(app_dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(app_dir.display().to_string())
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn download_file(url: String, filename: String) -> Result<(), String> {
|
||||
println!(
|
||||
"Starting download_file with url: {}, filename: {}",
|
||||
url, filename
|
||||
);
|
||||
let proj_dirs = get_project_dirs()?;
|
||||
let app_dir: PathBuf = proj_dirs.data_dir().to_path_buf();
|
||||
println!("App dir path: {:?}", app_dir);
|
||||
if !app_dir.exists() {
|
||||
println!("Creating app directory");
|
||||
fs::create_dir_all(&app_dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let url = url.clone();
|
||||
let filename = filename.clone();
|
||||
|
||||
// Use tokio::task::spawn_blocking for blocking operations
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let agent = ureq::Agent::config_builder()
|
||||
.max_redirects(20)
|
||||
.build()
|
||||
.new_agent();
|
||||
|
||||
let mut response = agent.get(&url).call().map_err(|e| e.to_string())?;
|
||||
let mut reader = response.body_mut().with_config().reader(); // Alternative approach
|
||||
let mut file = File::create(app_dir.join(&filename)).map_err(|e| e.to_string())?;
|
||||
copy(&mut reader, &mut file).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone, PartialEq, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct EpisodeInfo {
|
||||
pub episodetitle: String,
|
||||
pub podcastname: String,
|
||||
pub podcastid: i32,
|
||||
pub podcastindexid: Option<i64>,
|
||||
pub feedurl: String, // This field exists in the response
|
||||
pub episodepubdate: String,
|
||||
pub episodedescription: String,
|
||||
pub episodeartwork: String,
|
||||
pub episodeurl: String,
|
||||
pub episodeduration: i32,
|
||||
pub listenduration: Option<i32>,
|
||||
pub episodeid: i32,
|
||||
pub completed: bool,
|
||||
pub is_queued: bool,
|
||||
pub is_saved: bool,
|
||||
pub is_downloaded: bool,
|
||||
pub downloadedlocation: Option<String>,
|
||||
pub is_youtube: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone, PartialEq, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct EpisodeDownload {
|
||||
pub episodetitle: String,
|
||||
pub podcastname: String,
|
||||
pub episodepubdate: String,
|
||||
pub episodedescription: String,
|
||||
pub episodeartwork: String,
|
||||
pub episodeurl: String,
|
||||
pub episodeduration: i32,
|
||||
pub listenduration: Option<i32>,
|
||||
pub episodeid: i32,
|
||||
pub downloadedlocation: Option<String>,
|
||||
pub podcastid: i32,
|
||||
pub podcastindexid: Option<i64>,
|
||||
pub completed: bool,
|
||||
pub queued: bool,
|
||||
pub saved: bool,
|
||||
pub downloaded: bool,
|
||||
pub is_youtube: bool,
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn update_local_db(mut episode_info: EpisodeInfo) -> Result<(), String> {
|
||||
let proj_dirs = get_project_dirs().map_err(|e| e.to_string())?;
|
||||
let db_path = proj_dirs.data_dir().join("local_episodes.json");
|
||||
|
||||
// Calculate the downloaded location
|
||||
let download_dir = proj_dirs
|
||||
.data_dir()
|
||||
.join(format!("episode_{}.mp3", episode_info.episodeid));
|
||||
episode_info.downloadedlocation = Some(download_dir.to_string_lossy().into_owned());
|
||||
|
||||
let mut episodes = if db_path.exists() {
|
||||
let data = std::fs::read_to_string(&db_path).map_err(|e| e.to_string())?;
|
||||
serde_json::from_str::<Vec<EpisodeInfo>>(&data).map_err(|e| e.to_string())?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Check if episode already exists before adding
|
||||
if !episodes.iter().any(|ep| ep.episodeid == episode_info.episodeid) {
|
||||
episodes.push(episode_info);
|
||||
}
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&db_path)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_writer(file, &episodes).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn remove_multiple_from_local_db(episode_ids: Vec<i32>) -> Result<(), String> {
|
||||
let proj_dirs = get_project_dirs().map_err(|e| e.to_string())?;
|
||||
let db_path = proj_dirs.data_dir().join("local_episodes.json");
|
||||
|
||||
let mut episodes = if db_path.exists() {
|
||||
let data = std::fs::read_to_string(&db_path).map_err(|e| e.to_string())?;
|
||||
serde_json::from_str::<Vec<EpisodeInfo>>(&data).map_err(|e| e.to_string())?
|
||||
} else {
|
||||
return Ok(()); // No episodes to remove if file doesn't exist
|
||||
};
|
||||
|
||||
// Remove episodes with matching IDs
|
||||
episodes.retain(|episode| !episode_ids.contains(&episode.episodeid));
|
||||
|
||||
// Write updated episodes back to file
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&db_path)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_writer(file, &episodes).map_err(|e| e.to_string())?;
|
||||
|
||||
// Delete the audio files and artwork for each episode
|
||||
for episodeid in episode_ids {
|
||||
let audio_file_path = proj_dirs
|
||||
.data_dir()
|
||||
.join(format!("episode_{}.mp3", episodeid));
|
||||
let artwork_file_path = proj_dirs
|
||||
.data_dir()
|
||||
.join(format!("artwork_{}.jpg", episodeid));
|
||||
|
||||
if audio_file_path.exists() {
|
||||
std::fs::remove_file(audio_file_path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
if artwork_file_path.exists() {
|
||||
std::fs::remove_file(artwork_file_path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn remove_from_local_db(episodeid: i32) -> Result<(), String> {
|
||||
let proj_dirs = get_project_dirs().map_err(|e| e.to_string())?;
|
||||
let db_path = proj_dirs.data_dir().join("local_episodes.json");
|
||||
|
||||
let mut episodes = if db_path.exists() {
|
||||
let data = std::fs::read_to_string(&db_path).map_err(|e| e.to_string())?;
|
||||
serde_json::from_str::<Vec<EpisodeInfo>>(&data).map_err(|e| e.to_string())?
|
||||
} else {
|
||||
return Ok(()); // No episodes to remove if file doesn't exist
|
||||
};
|
||||
|
||||
episodes.retain(|episode| episode.episodeid != episodeid);
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&db_path)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_writer(file, &episodes).map_err(|e| e.to_string())?;
|
||||
|
||||
// Delete the audio file and artwork
|
||||
let audio_file_path = proj_dirs
|
||||
.data_dir()
|
||||
.join(format!("episode_{}.mp3", episodeid));
|
||||
let artwork_file_path = proj_dirs
|
||||
.data_dir()
|
||||
.join(format!("artwork_{}.jpg", episodeid));
|
||||
|
||||
if audio_file_path.exists() {
|
||||
std::fs::remove_file(audio_file_path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
if artwork_file_path.exists() {
|
||||
std::fs::remove_file(artwork_file_path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn deduplicate_local_episodes() -> Result<(), String> {
|
||||
let proj_dirs = get_project_dirs().map_err(|e| e.to_string())?;
|
||||
let db_path = proj_dirs.data_dir().join("local_episodes.json");
|
||||
|
||||
if !db_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let data = std::fs::read_to_string(&db_path).map_err(|e| e.to_string())?;
|
||||
let episodes = match serde_json::from_str::<Vec<EpisodeInfo>>(&data) {
|
||||
Ok(eps) => eps,
|
||||
Err(e) => {
|
||||
println!("JSON parsing error: {}, resetting file", e);
|
||||
std::fs::write(&db_path, "[]").map_err(|e| e.to_string())?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Remove duplicates based on episodeid
|
||||
let mut unique_episodes = Vec::new();
|
||||
let mut seen_ids = HashSet::new();
|
||||
|
||||
for episode in episodes {
|
||||
if seen_ids.insert(episode.episodeid) {
|
||||
unique_episodes.push(episode);
|
||||
}
|
||||
}
|
||||
|
||||
// Write back the deduplicated episodes
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&db_path)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_writer(file, &unique_episodes).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn get_local_episodes() -> Result<Vec<EpisodeDownload>, String> {
|
||||
let proj_dirs = get_project_dirs().map_err(|e| e.to_string())?;
|
||||
let db_path = proj_dirs.data_dir().join("local_episodes.json");
|
||||
|
||||
if !db_path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let data = std::fs::read_to_string(&db_path).map_err(|e| e.to_string())?;
|
||||
println!("Raw JSON data: {}", data);
|
||||
|
||||
// If JSON is corrupted, reset it and return empty
|
||||
let episodes = match serde_json::from_str::<Vec<EpisodeInfo>>(&data) {
|
||||
Ok(eps) => eps,
|
||||
Err(e) => {
|
||||
println!("JSON parsing error: {}, resetting file", e);
|
||||
// Reset the file to empty array
|
||||
std::fs::write(&db_path, "[]").map_err(|e| e.to_string())?;
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
};
|
||||
|
||||
// Convert EpisodeInfo to EpisodeDownload
|
||||
let converted_episodes: Vec<EpisodeDownload> = episodes
|
||||
.into_iter()
|
||||
.map(|ep| EpisodeDownload {
|
||||
episodetitle: ep.episodetitle,
|
||||
podcastname: ep.podcastname,
|
||||
episodepubdate: ep.episodepubdate,
|
||||
episodedescription: ep.episodedescription,
|
||||
episodeartwork: ep.episodeartwork,
|
||||
episodeurl: ep.episodeurl,
|
||||
episodeduration: ep.episodeduration,
|
||||
listenduration: ep.listenduration,
|
||||
episodeid: ep.episodeid,
|
||||
downloadedlocation: ep.downloadedlocation,
|
||||
podcastid: ep.podcastid,
|
||||
podcastindexid: ep.podcastindexid,
|
||||
completed: ep.completed,
|
||||
queued: ep.is_queued,
|
||||
saved: ep.is_saved,
|
||||
downloaded: ep.is_downloaded,
|
||||
is_youtube: ep.is_youtube,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(converted_episodes)
|
||||
}
|
||||
|
||||
#[command]
|
||||
fn delete_file(filename: String) -> Result<(), String> {
|
||||
let proj_dirs = get_project_dirs()?;
|
||||
let app_dir = proj_dirs.data_dir();
|
||||
let file_path = app_dir.join(filename);
|
||||
if file_path.exists() {
|
||||
fs::remove_file(file_path).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("File does not exist".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
fn list_app_files() -> Result<Vec<FileEntry>, String> {
|
||||
let proj_dirs = get_project_dirs()?;
|
||||
let app_dir = proj_dirs.data_dir();
|
||||
let mut entries = Vec::new();
|
||||
for entry in fs::read_dir(app_dir).map_err(|e| e.to_string())? {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
entries.push(FileEntry {
|
||||
path: entry.path().display().to_string(),
|
||||
});
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Serialize)]
|
||||
pub struct PodcastDetails {
|
||||
pub podcastid: i32,
|
||||
pub podcastindexid: Option<i64>,
|
||||
pub artworkurl: String,
|
||||
pub author: String,
|
||||
#[serde(deserialize_with = "deserialize_categories")]
|
||||
pub categories: HashMap<String, String>,
|
||||
pub description: String,
|
||||
pub episodecount: i32,
|
||||
pub explicit: bool,
|
||||
pub feedurl: String,
|
||||
pub podcastname: String,
|
||||
pub userid: i32,
|
||||
pub websiteurl: String,
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn update_podcast_db(podcast_details: PodcastDetails) -> Result<(), String> {
|
||||
let proj_dirs = get_project_dirs().map_err(|e| e.to_string())?;
|
||||
let db_path = proj_dirs.data_dir().join("local_podcasts.json");
|
||||
|
||||
let mut podcasts = if db_path.exists() {
|
||||
let data = std::fs::read_to_string(&db_path).map_err(|e| e.to_string())?;
|
||||
serde_json::from_str::<Vec<PodcastDetails>>(&data).map_err(|e| e.to_string())?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if !podcasts
|
||||
.iter()
|
||||
.any(|p| p.podcastid == podcast_details.podcastid)
|
||||
{
|
||||
podcasts.push(podcast_details);
|
||||
}
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&db_path)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_writer(file, &podcasts).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[allow(non_snake_case)]
|
||||
pub struct Podcast {
|
||||
pub podcastid: i32,
|
||||
pub podcastindexid: Option<i64>,
|
||||
pub podcastname: String,
|
||||
pub artworkurl: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub episodecount: i32,
|
||||
pub websiteurl: Option<String>,
|
||||
pub feedurl: String,
|
||||
pub author: Option<String>,
|
||||
#[serde(deserialize_with = "deserialize_categories")]
|
||||
pub categories: HashMap<String, String>,
|
||||
pub explicit: bool,
|
||||
// pub is_youtube: bool,
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn get_local_podcasts() -> Result<Vec<Podcast>, String> {
|
||||
let proj_dirs = get_project_dirs().map_err(|e| e.to_string())?;
|
||||
let db_path = proj_dirs.data_dir().join("local_podcasts.json");
|
||||
|
||||
if !db_path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let data = std::fs::read_to_string(&db_path).map_err(|e| e.to_string())?;
|
||||
let podcasts = serde_json::from_str::<Vec<Podcast>>(&data).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(podcasts)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_local_file(filepath: String) -> Result<Vec<u8>, String> {
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
|
||||
let path = PathBuf::from(filepath);
|
||||
let mut file = File::open(&path).map_err(|e| e.to_string())?;
|
||||
let mut buffer = Vec::new();
|
||||
file.read_to_end(&mut buffer).map_err(|e| e.to_string())?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn start_file_server(filepath: String) -> Result<String, String> {
|
||||
// Log the file path to ensure it's correct
|
||||
println!("Starting file server with path: {}", filepath);
|
||||
|
||||
// Ensure the path exists and is accessible
|
||||
if !std::path::Path::new(&filepath).exists() {
|
||||
return Err(format!("File path does not exist: {}", filepath));
|
||||
}
|
||||
|
||||
// Get the directory of the file
|
||||
let file_dir = std::path::Path::new(&filepath)
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
// Log the directory being served
|
||||
println!("Serving files from directory: {}", file_dir);
|
||||
|
||||
// Create the warp filter to serve the directory containing the file
|
||||
let file_route = warp::fs::dir(file_dir);
|
||||
|
||||
// Start the warp server
|
||||
tokio::spawn(warp::serve(file_route).run(([127, 0, 0, 1], 3030)));
|
||||
|
||||
Ok("http://127.0.0.1:3030".to_string())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
list_dir,
|
||||
get_app_dir,
|
||||
download_file,
|
||||
delete_file,
|
||||
update_local_db,
|
||||
remove_from_local_db,
|
||||
remove_multiple_from_local_db,
|
||||
update_podcast_db,
|
||||
get_local_podcasts,
|
||||
get_local_episodes,
|
||||
deduplicate_local_episodes,
|
||||
list_app_files,
|
||||
get_local_file,
|
||||
start_file_server
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
5
PinePods-0.8.2/web/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
}
|
||||
70
PinePods-0.8.2/web/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"build": {
|
||||
"beforeBuildCommand": "",
|
||||
"beforeDevCommand": "RUSTFLAGS='--cfg=web_sys_unstable_apis --cfg getrandom_backend=\"wasm_js\"' trunk serve",
|
||||
"devUrl": "http://localhost:8080",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"identifier": "com.gooseberrydevelopment.pinepods",
|
||||
"productName": "Pinepods",
|
||||
"version": "1.2.4",
|
||||
"app": {
|
||||
"trayIcon": {
|
||||
"iconPath": "icons/icon.png",
|
||||
"iconAsTemplate": true
|
||||
},
|
||||
"withGlobalTauri": true,
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"height": 1000,
|
||||
"resizable": true,
|
||||
"title": "Pinepods",
|
||||
"width": 1200
|
||||
}
|
||||
]
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": [],
|
||||
"files": {
|
||||
"/usr/share/metainfo/com.gooseberrydevelopment.pinepods.metainfo.xml": "./com.gooseberrydevelopment.pinepods.metainfo.xml"
|
||||
}
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"versionCode": 100
|
||||
},
|
||||
"externalBin": [],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
}
|
||||
}
|
||||