feat(): first working version

This commit is contained in:
2024-12-23 12:50:01 +01:00
commit a6d0d34ad1
4 changed files with 1981 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
/target
.DS_Store
Generated
+1708
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "photoview-dl"
version = "0.1.0"
edition = "2021"
authors = ["David Schütte"]
[dependencies]
clap = { version = "4.5.23", features = ["derive", "std"] }
futures = "0.3.31"
reqwest = { version = "0.12.9", features = ["json", "stream"] }
serde = { version = "1.0.216", features = ["derive"] }
tokio = { version = "1.42.0", default-features = false, features = ["full"] }
tokio-util = "0.7.13"
+258
View File
@@ -0,0 +1,258 @@
use futures::TryFutureExt;
use reqwest::{Client, Url};
use futures::stream::TryStreamExt;
use tokio::fs::File;
use tokio::io::copy;
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::fmt::format;
use std::sync::Arc;
use std::{collections::HashMap, error::Error, path::PathBuf};
use tokio::sync::Semaphore;
use tokio_util::io::StreamReader;
/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
/// URL of share link
url: Url,
/// The path to the directory to place all downloads in
path: PathBuf,
}
#[derive(Serialize)]
struct GraphQLBody<'r> {
#[serde(rename(serialize = "operationName"))]
operation_name: String,
query: String,
variables: &'r HashMap<String, String>,
}
#[derive(Deserialize)]
struct AlbumInfoResponse3 {
id: String,
}
#[derive(Deserialize)]
struct AlbumInfoResponse2 {
album: AlbumInfoResponse3,
}
#[derive(Deserialize)]
struct AlbumInfoResponse1 {
#[serde(rename(deserialize = "shareToken"))]
share_token: AlbumInfoResponse2,
}
#[derive(Deserialize)]
struct AlbumInfoResponse {
data: AlbumInfoResponse1,
}
#[derive(Deserialize)]
struct MediaUrl {
url: String,
#[serde(rename(deserialize = "fileSize"))]
file_size: u64,
}
#[derive(Deserialize)]
struct MediaDownload {
title: String,
#[serde(rename(deserialize = "mediaUrl"))]
media_url: MediaUrl,
}
#[derive(Deserialize)]
struct MediaItem {
title: String,
downloads: Vec<MediaDownload>,
}
#[derive(Deserialize)]
struct MediaInfoResponse2 {
media: Vec<MediaItem>,
}
#[derive(Deserialize)]
struct MediaInfoResponse1 {
album: MediaInfoResponse2,
}
#[derive(Deserialize)]
struct MediaInfoResponse {
data: MediaInfoResponse1,
}
async fn download_file(
file: MediaItem,
client: Arc<Client>,
share_token: String,
query_url: Url,
cli: Arc<Cli>,
sem: Arc<Semaphore>,
) -> Result<(), std::io::Error> {
let _permit = sem.clone().acquire_owned().await;
let mut file = file;
file.downloads.retain(|i| i.title == "Original");
let selected_download = &file.downloads[0].media_url;
let mut url = query_url;
url.set_path(&selected_download.url);
let resp = client
.get(url)
.query(&[("token", share_token)])
.send()
.await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
let mut filepath = cli.path.clone();
filepath.push(&file.title);
let fd = File::open(&filepath).await;
if let Ok(fd) = fd {
if let Ok(metadata) = fd.metadata().await {
println!("Skipping {}", file.title);
if metadata.len() == selected_download.file_size {
return Ok(());
}
}
}
println!("Downloading {}", file.title);
let mut fd = File::create(&filepath).await?;
let mut reader = StreamReader::new(
resp.bytes_stream()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)),
);
copy(&mut reader, &mut fd).await?;
Ok(())
}
async fn media_info() {}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let cli = Arc::new(Cli::parse());
let share_token = cli.clone().url.path()[7..].to_owned();
let mut query_url = cli.url.clone();
query_url.set_path("/api/graphql");
let client = Arc::new(reqwest::Client::new());
println!("Parsed share token: {}", share_token);
let mut album_info_vars = HashMap::new();
album_info_vars.insert("token".to_owned(), share_token.clone());
let album_info_body = GraphQLBody {
operation_name: "SharePageToken".to_owned(),
query: r"query SharePageToken($token: String!, $password: String) {shareToken(credentials: {token: $token, password: $password}) {album {id}}}".to_owned(),
variables: &album_info_vars
};
let album_info = client
.post(query_url.clone())
.json(&album_info_body)
.send()
.await?;
// println!("{}", album_info?.text()?);
let id = album_info
.json::<AlbumInfoResponse>()
.await?
.data
.share_token
.album
.id;
println!("Found album id: {}", id);
let mut all_files: Vec<MediaItem> = Vec::new();
let batch_size = 200;
let mut offset = 0;
let mut media_vars = HashMap::new();
media_vars.insert("id".to_owned(), id.clone());
media_vars.insert("mediaOrderBy".to_owned(), "date_shot".to_owned());
media_vars.insert("mediaOrderDirection".to_owned(), "ASC".to_owned());
media_vars.insert("token".to_owned(), share_token.clone());
loop {
media_vars.insert("limit".to_owned(), format!("{}", batch_size));
media_vars.insert("offset".to_owned(), format!("{}", offset));
let media_body = GraphQLBody{
operation_name: "shareAlbumQuery".to_owned(),
query: "query shareAlbumQuery($id: ID!, $token: String!, $password: String, $mediaOrderBy: String, $mediaOrderDirection: OrderDirection, $limit: Int, $offset: Int) {
album(id: $id, tokenCredentials: {token: $token, password: $password}) {
media(
paginate: {limit: $limit, offset: $offset}
order: {order_by: $mediaOrderBy, order_direction: $mediaOrderDirection}
) {
title
downloads {
title
mediaUrl {
url
width
height
fileSize
__typename
}
__typename
}
__typename
}
__typename
}
}
".to_owned(),
variables: &media_vars
};
let media = client
.post(query_url.clone())
.json(&media_body)
.send()
.await?;
let mut files = media.json::<MediaInfoResponse>().await?.data.album.media;
let len = files.len();
all_files.append(&mut files);
if len < 200 {
break;
}
offset += len;
}
let downloads = all_files;
let len = downloads.len();
println!("Total files: {}", len);
let mut handles = Vec::with_capacity(len);
let sem = Arc::new(Semaphore::new(10));
for file in downloads {
let client = client.clone();
let cli = cli.clone();
let sem = sem.clone();
let job = tokio::spawn(download_file(
file,
client,
share_token.clone(),
query_url.clone(),
cli,
sem,
));
handles.push(job);
}
for job in handles {
job.await;
}
Ok(())
}