diff --git a/Cargo.lock b/Cargo.lock
index dbbb8ce..7672f63 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,21 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
 
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "anstream"
 version = "0.6.19"
@@ -133,6 +148,21 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
 [[package]]
 name = "clap"
 version = "4.5.39"
@@ -534,6 +564,30 @@ dependencies = [
  "windows-registry",
 ]
 
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
 [[package]]
 name = "icu_collections"
 version = "2.0.0"
@@ -762,6 +816,15 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "object"
 version = "0.36.7"
@@ -860,6 +923,15 @@ dependencies = [
  "zerovec",
 ]
 
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.95"
@@ -884,6 +956,35 @@ version = "5.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
 
+[[package]]
+name = "rand"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
 [[package]]
 name = "reqwest"
 version = "0.12.19"
@@ -942,10 +1043,11 @@ dependencies = [
 
 [[package]]
 name = "rreview"
-version = "1.0.0"
+version = "1.1.0"
 dependencies = [
  "clap",
  "futures",
+ "rand",
  "siphasher",
  "submissions-api",
  "tokio",
@@ -1153,10 +1255,11 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
 [[package]]
 name = "submissions-api"
-version = "0.7.2"
+version = "0.8.1"
 source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
-checksum = "f0a098bbc31ecadff0237a2979cda54b2369c76e3a1ce7b86ff4643bfbf4822f"
+checksum = "a08deea49e9e34f2f2f23219f4a565681b4c1ae46f8012496d9a8fe10897efd3"
 dependencies = [
+ "chrono",
  "reqwest",
  "serde",
  "serde_json",
@@ -1521,6 +1624,41 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "windows-link"
 version = "0.1.1"
@@ -1677,6 +1815,26 @@ dependencies = [
  "synstructure",
 ]
 
+[[package]]
+name = "zerocopy"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "zerofrom"
 version = "0.1.6"
diff --git a/Cargo.toml b/Cargo.toml
index 95d6e97..f83cfbd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "rreview"
-version = "1.0.0"
+version = "1.1.0"
 edition = "2021"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -8,8 +8,9 @@ edition = "2021"
 [dependencies]
 clap = { version = "4.4.2", features = ["derive"] }
 futures = "0.3.31"
+rand = "0.9.1"
 siphasher = "1.0.1"
-submissions-api = { version = "0.7.2", registry = "strafesnet" }
+submissions-api = { version = "0.8.1", registry = "strafesnet" }
 tokio = { version = "1.42.0", features = ["fs", "macros", "rt-multi-thread"] }
 
 [profile.release]
diff --git a/src/main.rs b/src/main.rs
index 1027d6c..f6995c8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,7 @@
 use clap::{Args,Parser,Subcommand};
 use futures::{StreamExt,TryStreamExt};
+use rand::seq::SliceRandom;
+use std::io::Write;
 use std::path::PathBuf;
 
 const READ_CONCURRENCY:usize=16;
@@ -15,12 +17,20 @@ struct Cli{
 
 #[derive(Subcommand)]
 enum Commands{
+	Release(ReleaseCommand),
 	RepairDuplicates(RepairDuplicatesCommand),
 	RepairPolicies(RepairPoliciesCommand),
 	Review(ReviewCommand),
 	UploadScripts(UploadScriptsCommand),
 }
 
+#[derive(Args)]
+struct ReleaseCommand{
+	#[arg(long)]
+	session_id_file:PathBuf,
+	#[arg(long)]
+	api_url:String,
+}
 #[derive(Args)]
 struct RepairDuplicatesCommand{
 	#[arg(long)]
@@ -54,6 +64,10 @@ struct UploadScriptsCommand{
 async fn main(){
 	let cli=Cli::parse();
 	match cli.command{
+		Commands::Release(command)=>release(ReleaseConfig{
+			session_id:std::fs::read_to_string(command.session_id_file).unwrap(),
+			api_url:command.api_url,
+		}).await.unwrap(),
 		Commands::RepairDuplicates(command)=>repair_duplicates(RepairDuplicatesConfig{
 			session_id:std::fs::read_to_string(command.session_id_file).unwrap(),
 			api_url:command.api_url,
@@ -103,13 +117,13 @@ enum ReviewError{
 	Cookie(submissions_api::CookieError),
 	Reqwest(submissions_api::ReqwestError),
 	GetPolicies(submissions_api::Error),
-	GetScriptFromHash(submissions_api::types::SingleItemError),
+	GetScriptFromHash(submissions_api::types::ScriptSingleItemError),
 	NoScript,
 	WriteCurrent(std::io::Error),
 	ActionIO(std::io::Error),
 	PurgeScript(submissions_api::Error),
 	ReadCurrent(std::io::Error),
-	DeduplicateModified(submissions_api::types::SingleItemError),
+	DeduplicateModified(submissions_api::types::ScriptSingleItemError),
 	UploadModified(submissions_api::Error),
 	UpdateScriptPolicy(submissions_api::Error),
 }
@@ -236,10 +250,10 @@ enum ScriptUploadError{
 	AllowedMap(GetMapError),
 	ReplaceMap(GetMapError),
 	BlockedSet(std::io::Error),
-	GetOrCreate(GOCError),
-	GetOrCreatePolicyReplace(GOCError),
-	GetOrCreatePolicyAllowed(GOCError),
-	GetOrCreatePolicyBlocked(GOCError),
+	GetOrCreate(GOCScriptError),
+	GetOrCreatePolicyReplace(GOCScriptPolicyError),
+	GetOrCreatePolicyAllowed(GOCScriptPolicyError),
+	GetOrCreatePolicyBlocked(GOCScriptPolicyError),
 }
 
 fn read_dir_stream(dir:tokio::fs::ReadDir)->impl futures::stream::Stream<Item=std::io::Result<tokio::fs::DirEntry>>{
@@ -318,10 +332,11 @@ fn hash_format(hash:u64)->String{
 	format!("{:016x}",hash)
 }
 
-type GOCError=submissions_api::types::SingleItemError;
-type GOCResult=Result<submissions_api::types::ScriptID,GOCError>;
+type GOCScriptError=submissions_api::types::ScriptSingleItemError;
+type GOCScriptPolicyError=submissions_api::types::ScriptPolicySingleItemError;
+type GOCScriptResult=Result<submissions_api::types::ScriptID,GOCScriptError>;
 
-async fn get_or_create_script(api:&submissions_api::external::Context,source:&str)->GOCResult{
+async fn get_or_create_script(api:&submissions_api::external::Context,source:&str)->GOCScriptResult{
 	let script_response=api.get_script_from_hash(submissions_api::types::HashRequest{
 		hash:hash_format(hash_source(source)).as_str(),
 	}).await?;
@@ -333,7 +348,7 @@ async fn get_or_create_script(api:&submissions_api::external::Context,source:&st
 			Source:source,
 			ResourceType:submissions_api::types::ResourceType::Unknown,
 			ResourceID:None,
-		}).await.map_err(GOCError::Other)?.ScriptID
+		}).await.map_err(GOCScriptError::Other)?.ScriptID
 	})
 }
 
@@ -341,7 +356,7 @@ async fn check_or_create_script_poicy(
 	api:&submissions_api::external::Context,
 	hash:&str,
 	script_policy:submissions_api::types::CreateScriptPolicyRequest,
-)->Result<(),GOCError>{
+)->Result<(),GOCScriptPolicyError>{
 	let script_policy_result=api.get_script_policy_from_hash(submissions_api::types::HashRequest{
 		hash,
 	}).await?;
@@ -355,7 +370,7 @@ async fn check_or_create_script_poicy(
 		},
 		None=>{
 			// create a new policy
-			api.create_script_policy(script_policy).await.map_err(GOCError::Other)?;
+			api.create_script_policy(script_policy).await.map_err(GOCScriptPolicyError::Other)?;
 		}
 	}
 
@@ -464,7 +479,7 @@ enum RepairPoliciesError{
 	Cookie(submissions_api::CookieError),
 	Reqwest(submissions_api::ReqwestError),
 	GetPolicies(submissions_api::Error),
-	GetScripts(submissions_api::types::SingleItemError),
+	GetScripts(submissions_api::types::ScriptSingleItemError),
 	UpdateScriptPolicy(submissions_api::Error),
 }
 
@@ -577,3 +592,158 @@ async fn repair_duplicates(config:RepairDuplicatesConfig)->Result<(),RepairDupli
 
 	Ok(())
 }
+
+#[allow(dead_code)]
+#[derive(Debug)]
+enum ReleaseError{
+	Cookie(submissions_api::CookieError),
+	Reqwest(submissions_api::ReqwestError),
+	GetSubmissions(submissions_api::Error),
+	GetMaps(submissions_api::Error),
+	Io(std::io::Error),
+	Release(submissions_api::Error),
+}
+
+struct ReleaseConfig{
+	session_id:String,
+	api_url:String,
+}
+async fn release(config:ReleaseConfig)->Result<(),ReleaseError>{
+	let cookie=submissions_api::Cookie::new(&config.session_id).map_err(ReleaseError::Cookie)?;
+	let api=&submissions_api::external::Context::new(config.api_url,cookie).map_err(ReleaseError::Reqwest)?;
+
+	const LIMIT:u32=100;
+	const ONE_HOUR:i64=60*60;
+	const ONE_DAY:i64=24*ONE_HOUR;
+	const ONE_WEEK:i64=7*ONE_DAY;
+	const FRIDAY:i64=2*ONE_DAY;
+	const PEAK_HOURS:i64=-7*ONE_HOUR;
+
+	// determine maps ready to be released
+	let mut submissions_pending_release=std::collections::BTreeMap::new();
+	{
+		println!("Downloading submissions pending release...");
+		let mut page=1;
+		loop{
+			let submissions=api.get_submissions(submissions_api::types::GetSubmissionsRequest{
+				Page:page,
+				Limit:LIMIT,
+				DisplayName:None,
+				Creator:None,
+				GameID:None,
+				Sort:None,
+				Submitter:None,
+				AssetID:None,
+				UploadedAssetID:None,
+				StatusID:Some(submissions_api::types::SubmissionStatus::Uploaded),
+			}).await.map_err(ReleaseError::GetSubmissions)?;
+			let len=submissions.Submissions.len();
+			for submission in submissions.Submissions{
+				submissions_pending_release.entry(submission.GameID).or_insert(Vec::new()).push(submission);
+			}
+			if len<LIMIT as usize{
+				break;
+			}else{
+				page+=1;
+			}
+		}
+	}
+	// If there is nothing to release, exit immediately
+	if submissions_pending_release.is_empty(){
+		println!("Nothing to release!");
+		return Ok(());
+	}
+
+	// determine the most recent map release date
+	// if it's in the past, generate a Friday 10AM timestamp instead
+	let it={
+		println!("Determining most recent release dates...");
+		let mut latest_date=std::collections::HashMap::new();
+		let mut page=1;
+		loop{
+			let maps=api.get_maps(submissions_api::types::GetMapsRequest{
+				Page:page,
+				Limit:LIMIT,
+				DisplayName:None,
+				Creator:None,
+				GameID:None,
+				Sort:None,//TODO: sort by date to cut down requests
+			}).await.map_err(ReleaseError::GetMaps)?;
+			let len=maps.len();
+			for map in maps{
+				latest_date
+					.entry(map.GameID)
+					.and_modify(|date|
+						*date=map.Date.min(*date)
+					)
+					.or_insert(map.Date);
+			}
+			if len<LIMIT as usize{
+				break;
+			}else{
+				page+=1;
+			}
+		}
+
+		// breaks on Sun 4 Dec 292277026596
+		let now=std::time::UNIX_EPOCH.elapsed().unwrap().as_secs() as i64;
+
+		// If the date is in the past, unset it
+		latest_date.retain(|_,&mut date|now<date);
+
+		submissions_pending_release.into_iter().map(move|(game,pending)|{
+			let start_date=match latest_date.get(&game){
+				Some(&date)=>{
+					// round to friday
+					(date+(ONE_WEEK>>1)-FRIDAY)/ONE_WEEK*ONE_WEEK+FRIDAY+PEAK_HOURS
+					// add a week
+					+ONE_WEEK
+				},
+				// find soonest friday
+				None=>((now-FRIDAY) as u64).next_multiple_of(ONE_WEEK as u64) as i64+FRIDAY+PEAK_HOURS
+			};
+
+			(game,start_date,pending)
+		})
+	};
+
+	let mut rng=rand::rng();
+
+	for (game,start_date,mut pending) in it{
+		// shuffle maps
+		pending.shuffle(&mut rng);
+
+		// schedule one per week
+		let schedule:&Vec<_>=&pending.into_iter().enumerate().map(|(i,submission)|{
+			let release_date=(std::time::UNIX_EPOCH+std::time::Duration::from_secs((
+				start_date+i as i64*ONE_WEEK
+			) as u64)).into();
+			println!("Schedule {:?} {} at {}",submission.ID,submission.DisplayName,release_date);
+			submissions_api::types::ReleaseInfo{
+				Date:release_date,
+				SubmissionID:submission.ID,
+			}
+		}).collect();
+
+		// ask to confirm schedule
+		print!("Accept this release schedule for {game:?}? [y/N]: ");
+		std::io::stdout().flush().map_err(ReleaseError::Io)?;
+
+		let mut input=String::new();
+		std::io::stdin().read_line(&mut input).map_err(ReleaseError::Io)?;
+		match input.trim(){
+			"y"|"Y"=>(),
+			_=>{
+				println!("Quitting.");
+				return Ok(());
+			},
+		}
+
+		// send it
+		api.release_submissions(submissions_api::types::ReleaseRequest{
+			schedule,
+		}).await.map_err(ReleaseError::Release)?;
+	}
+
+	Ok(())
+}