diff --git a/src/main.rs b/src/main.rs
index 0c08408fade7481910fa48d9d3f51b3b764d6d29..1a5355a50249d8cbb0cbcbb9f32d2088a50296da 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,17 +3,18 @@
 #![forbid(elided_lifetimes_in_paths, unsafe_code)]
 
 mod iotro;
+mod preset;
 mod project;
 mod question;
 mod render;
 mod time;
 
 use self::{
-	iotro::Language,
 	project::{Project, ProjectLecture, ProjectSource, Resolution},
 	render::Renderer,
 	time::{parse_date, parse_time, Time}
 };
+use crate::preset::Preset;
 use camino::Utf8PathBuf as PathBuf;
 use clap::Parser;
 use console::style;
@@ -34,34 +35,24 @@ struct Args {
 	#[clap(short = 'C', long, default_value = ".")]
 	directory: PathBuf,
 
-	/// The slug of the course, e.g. "23ws-malo2".
-	#[clap(short = 'c', long, default_value = "23ws-malo2")]
-	course: String,
-
-	/// The label of the course, e.g. "Mathematische Logik II".
-	#[clap(short, long, default_value = "Mathematische Logik II")]
-	label: String,
-
-	/// The docent of the course, e.g. "Prof. E. Grädel".
-	#[clap(short, long, default_value = "Prof. E. Grädel")]
-	docent: String,
-
-	/// The language of the lecture. Used for the intro and outro frame.
-	#[clap(short = 'L', long, default_value = "de")]
-	lang: Language<'static>,
+	/// The preset of the lecture. Can be a toml file or a known course slug.
+	#[clap(short, long)]
+	preset: String,
 
 	#[cfg(feature = "mem_limit")]
 	/// The memory limit for external tools like ffmpeg.
 	#[clap(short, long, default_value = "12G")]
 	mem_limit: String,
 
-	/// Transcode the final video clip down to the minimum resolution specified.
+	/// Transcode the final video clip down to the minimum resolution specified. If not
+	/// specified, the default value from the preset is used.
 	#[clap(short, long)]
 	transcode: Option<Resolution>,
 
 	/// Transcode starts at this resolution, or the source resolution, whichever is lower.
-	#[clap(short = 'T', long, default_value = "1440p")]
-	transcode_start: Resolution,
+	/// If not specified, the default value from the preset is used.
+	#[clap(short = 'T', long)]
+	transcode_start: Option<Resolution>,
 
 	/// Treat the audio as stereo. By default, only one channel from the input stereo will
 	/// be used, assuming either the other channel is backup or the same as the used.
@@ -125,7 +116,8 @@ fn main() {
 
 	// process arguments
 	let directory = args.directory.canonicalize_utf8().unwrap();
-	let course = args.course;
+	let preset = Preset::find(&args.preset).unwrap();
+	let course = preset.course;
 
 	// let's see if we need to initialise the project
 	let project_path = directory.join("project.toml");
@@ -170,10 +162,10 @@ fn main() {
 		let project = Project {
 			lecture: ProjectLecture {
 				course,
-				label: args.label,
-				docent: args.docent,
+				label: preset.label,
+				docent: preset.docent,
 				date,
-				lang: args.lang
+				lang: preset.lang
 			},
 			source: ProjectSource {
 				files,
@@ -282,10 +274,10 @@ fn main() {
 	});
 
 	// rescale the video
-	if let Some(lowest_res) = args.transcode {
+	if let Some(lowest_res) = args.transcode.or(preset.transcode) {
 		for res in Resolution::values().into_iter().rev() {
 			if res > project.source.metadata.as_ref().unwrap().source_res
-				|| res > args.transcode_start
+				|| res > args.transcode_start.unwrap_or(preset.transcode_start)
 				|| res < lowest_res
 			{
 				continue;
diff --git a/src/preset.rs b/src/preset.rs
new file mode 100644
index 0000000000000000000000000000000000000000..85f37c604e32091f1748552fdab65c88b44784d3
--- /dev/null
+++ b/src/preset.rs
@@ -0,0 +1,77 @@
+//! This struct defines presets.
+
+use crate::{
+	iotro::{Language, BRITISH, GERMAN},
+	project::Resolution
+};
+use anyhow::bail;
+use serde::{Deserialize, Serialize};
+use serde_with::{serde_as, DisplayFromStr};
+use std::{fs, io};
+
+#[serde_as]
+#[derive(Deserialize, Serialize)]
+pub(crate) struct Preset {
+	// options for the intro slide
+	pub(crate) course: String,
+	pub(crate) label: String,
+	pub(crate) docent: String,
+
+	/// Course language
+	#[serde(default = "Default::default")]
+	#[serde_as(as = "DisplayFromStr")]
+	pub(crate) lang: Language<'static>,
+
+	// coding options
+	pub(crate) transcode_start: Resolution,
+	pub(crate) transcode: Option<Resolution>
+}
+
+fn preset_23ws_malo2() -> Preset {
+	Preset {
+		course: "23ws-malo2".into(),
+		label: "Mathematische Logik II".into(),
+		docent: "Prof. E. Grädel".into(),
+		lang: GERMAN,
+		transcode_start: Resolution::WQHD,
+		transcode: Some(Resolution::nHD)
+	}
+}
+
+fn preset_24ss_algomod() -> Preset {
+	Preset {
+		course: "24ss-algomod".into(),
+		label: "Algorithmische Modelltheorie".into(),
+		docent: "Prof. E. Grädel".into(),
+		lang: GERMAN,
+		transcode_start: Resolution::WQHD,
+		transcode: Some(Resolution::HD)
+	}
+}
+
+fn preset_24ss_qc() -> Preset {
+	Preset {
+		course: "24ss-qc".into(),
+		label: "Introduction to Quantum Computing".into(),
+		docent: "Prof. D. Unruh".into(),
+		lang: BRITISH,
+		transcode_start: Resolution::WQHD,
+		transcode: Some(Resolution::HD)
+	}
+}
+
+impl Preset {
+	pub(crate) fn find(name: &str) -> anyhow::Result<Self> {
+		match fs::read(name) {
+			Ok(buf) => return Ok(toml::from_slice(&buf)?),
+			Err(err) if err.kind() == io::ErrorKind::NotFound => {},
+			Err(err) => return Err(err.into())
+		}
+		Ok(match name {
+			"23ws-malo2" => preset_23ws_malo2(),
+			"24ss-algomod" => preset_24ss_algomod(),
+			"24ss-qc" => preset_24ss_qc(),
+			_ => bail!("Unknown preset {name:?}")
+		})
+	}
+}
diff --git a/src/project.rs b/src/project.rs
index 46ed082ad79abc6d096f8833a4dc9cd24e55a8d2..7dbda1fc23f182b73dda6455b63fe28065c38448 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -1,3 +1,5 @@
+//! Defines the structure of the `project.toml` file.
+
 use crate::{
 	iotro::Language,
 	render::ffmpeg::FfmpegOutputFormat,