feat(): first working version
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
.DS_Store
|
||||
Generated
+1708
File diff suppressed because it is too large
Load Diff
+13
@@ -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
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user