steamroom_cli/commands/
workshop.rs1use crate::cli::WorkshopArgs;
8use crate::commands::shared::decompress_manifest;
9use crate::commands::shared::fmt_size;
10use crate::download as direct_progress;
11use crate::errors::CliError;
12use crate::sink::JobSink;
13use std::path::PathBuf;
14use std::sync::Arc;
15use steamroom::cdn::CdnClient;
16use steamroom::client::LoggedIn;
17use steamroom::client::SteamClient;
18use steamroom::depot::manifest::DepotManifest;
19use steamroom::depot::*;
20use tokio_util::sync::CancellationToken;
21use tracing::info;
22
23pub async fn run_workshop(
24 args: WorkshopArgs,
25 client: SteamClient<LoggedIn>,
26 sink: Arc<dyn JobSink>,
27 cancel: CancellationToken,
28 show_progress: bool,
29) -> Result<(), CliError> {
30 info!("fetching workshop item {} details...", args.item);
31 let req = steamroom::generated::CPublishedFileGetDetailsRequest {
32 publishedfileids: vec![args.item],
33 includechildren: Some(true),
34 ..Default::default()
35 };
36 let resp = client
37 .call_service_method(
38 "PublishedFile.GetDetails#1",
39 &prost::Message::encode_to_vec(&req),
40 )
41 .await?;
42 let details: steamroom::generated::CPublishedFileGetDetailsResponse = resp.decode()?;
43
44 let item = details
45 .publishedfiledetails
46 .first()
47 .ok_or(CliError::NoProductInfo(args.app))?;
48
49 let title = item.title.as_deref().unwrap_or("(untitled)");
50 let hcontent = item.hcontent_file.unwrap_or(0);
51 let file_size = item.file_size.unwrap_or(0);
52 let consumer_app = item.consumer_appid.unwrap_or(args.app);
53 let filename = item.filename.as_deref().unwrap_or("workshop_content");
54
55 info!("workshop item: {title}");
56 info!(" content manifest: {hcontent}");
57 info!(" file: {filename} ({} bytes)", file_size);
58
59 if hcontent == 0 {
60 info!("no downloadable content for this workshop item");
61 return Ok(());
62 }
63
64 let app_id = AppId(consumer_app);
66 let depot_id = DepotId(consumer_app);
67 let manifest_id = ManifestId(hcontent);
68
69 let depot_key = client.get_depot_decryption_key(depot_id, app_id).await?;
70 let cdn_servers = client.get_cdn_servers(CellId(0), Some(5)).await?;
71 if cdn_servers.is_empty() {
72 return Err(CliError::NoCdnServers);
73 }
74 let cdn_server = &cdn_servers[0];
75 let cdn_pool = steamroom::cdn::CdnServerPool::new(cdn_servers.clone());
76 let cdn = CdnClient::new().map_err(CliError::Steam)?;
77
78 let request_code = client
79 .get_manifest_request_code(app_id, depot_id, manifest_id, None, None)
80 .await?
81 .unwrap_or(0);
82
83 let manifest_data = cdn
84 .download_manifest(cdn_server, depot_id, manifest_id, request_code, None)
85 .await?;
86 let manifest_bytes = decompress_manifest(&manifest_data)?;
87 let mut manifest = DepotManifest::parse(&manifest_bytes)?;
88 if manifest.filenames_encrypted {
89 manifest.decrypt_filenames(&depot_key)?;
90 }
91
92 let output_dir = args
93 .output
94 .unwrap_or_else(|| PathBuf::from("workshop").join(args.item.to_string()));
95 std::fs::create_dir_all(&output_dir)?;
96
97 let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
98 let fetcher = steamroom_client::download::CdnChunkFetcher::new(cdn, cdn_pool, None);
99 let job = steamroom_client::download::DepotJob::builder()
100 .depot_id(depot_id)
101 .depot_key(depot_key)
102 .install_dir(output_dir.clone())
103 .event_sender(event_tx)
104 .build()
105 .map_err(|e| CliError::Io(std::io::Error::other(e)))?;
106
107 info!("downloading to {}", output_dir.display());
108
109 let progress_handle =
110 direct_progress::spawn_progress_renderer(event_rx, show_progress, Some(sink.clone()));
111
112 let stats_result = {
117 let download_fut = job.download(&manifest, std::sync::Arc::new(fetcher));
118 tokio::pin!(download_fut);
119 tokio::select! {
120 res = &mut download_fut => Some(res.map_err(|e| CliError::Io(std::io::Error::other(e)))),
121 _ = cancel.cancelled() => None,
125 }
126 };
127 let _ = progress_handle.await;
129 let stats = match stats_result {
130 Some(res) => res?,
131 None => return Err(CliError::Cancelled),
132 };
133
134 info!(
135 "workshop download complete: {} files, {}",
136 stats.files_completed,
137 fmt_size(stats.bytes_downloaded)
138 );
139 Ok(())
140}