diff --git a/src/main.rs b/src/main.rs
index 742ecd894945b4af74b755a1f529773e3c404a34..0c08408fade7481910fa48d9d3f51b3b764d6d29 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,30 +3,26 @@
 #![forbid(elided_lifetimes_in_paths, unsafe_code)]
 
 mod iotro;
+mod project;
 mod question;
 mod render;
 mod time;
 
 use self::{
 	iotro::Language,
-	question::Question,
-	render::{ffmpeg::FfmpegOutputFormat, Renderer},
-	time::{parse_date, parse_time, Date, Time}
+	project::{Project, ProjectLecture, ProjectSource, Resolution},
+	render::Renderer,
+	time::{parse_date, parse_time, Time}
 };
 use camino::Utf8PathBuf as PathBuf;
 use clap::Parser;
 use console::style;
-use rational::Rational;
-use serde::{Deserialize, Serialize};
-use serde_with::{serde_as, DisplayFromStr};
 #[cfg(feature = "mem_limit")]
 use std::sync::RwLock;
 use std::{
-	collections::BTreeSet,
 	fmt::Display,
 	fs,
-	io::{self, BufRead as _, Write},
-	str::FromStr
+	io::{self, BufRead as _, Write}
 };
 
 #[cfg(feature = "mem_limit")]
@@ -73,159 +69,6 @@ struct Args {
 	stereo: bool
 }
 
-macro_rules! resolutions {
-	($($res:ident: $width:literal x $height:literal at $bitrate:literal in $format:ident),+) => {
-		#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
-		#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
-		enum Resolution {
-			$(
-				#[doc = concat!(stringify!($width), "x", stringify!($height))]
-				$res
-			),+
-		}
-
-		const NUM_RESOLUTIONS: usize = {
-			let mut num = 0;
-			$(num += 1; stringify!($res);)+
-			num
-		};
-
-		impl Resolution {
-			fn values() -> [Self; NUM_RESOLUTIONS] {
-				[$(Self::$res),+]
-			}
-
-			fn width(self) -> usize {
-				match self {
-					$(Self::$res => $width),+
-				}
-			}
-
-			fn height(self) -> usize {
-				match self {
-					$(Self::$res => $height),+
-				}
-			}
-
-			fn bitrate(self) -> u64 {
-				match self {
-					$(Self::$res => $bitrate),+
-				}
-			}
-
-			fn format(self) -> FfmpegOutputFormat {
-				match self {
-					$(Self::$res => FfmpegOutputFormat::$format),+
-				}
-			}
-		}
-
-		impl FromStr for Resolution {
-			type Err = anyhow::Error;
-
-			fn from_str(s: &str) -> anyhow::Result<Self> {
-				Ok(match s {
-					$(concat!(stringify!($height), "p") => Self::$res,)+
-					_ => anyhow::bail!("Unknown Resolution: {s:?}")
-				})
-			}
-		}
-	}
-}
-
-resolutions! {
-	nHD: 640 x 360 at 500_000 in AvcAac,
-	HD: 1280 x 720 at 1_000_000 in AvcAac,
-	FullHD: 1920 x 1080 at 750_000 in Av1Opus,
-	WQHD: 2560 x 1440 at 1_000_000 in Av1Opus,
-	// TODO qsx muss mal sagen wieviel bitrate für 4k
-	UHD: 3840 x 2160 at 2_000_000 in Av1Opus
-}
-
-#[derive(Deserialize, Serialize)]
-struct Project {
-	lecture: ProjectLecture,
-	source: ProjectSource,
-	progress: ProjectProgress
-}
-
-#[serde_as]
-#[derive(Deserialize, Serialize)]
-struct ProjectLecture {
-	course: String,
-	label: String,
-	docent: String,
-	#[serde_as(as = "DisplayFromStr")]
-	date: Date,
-	#[serde(default = "Default::default")]
-	#[serde_as(as = "DisplayFromStr")]
-	lang: Language<'static>
-}
-
-#[serde_as]
-#[derive(Deserialize, Serialize)]
-struct ProjectSource {
-	files: Vec<String>,
-	stereo: bool,
-
-	#[serde_as(as = "Option<DisplayFromStr>")]
-	start: Option<Time>,
-	#[serde_as(as = "Option<DisplayFromStr>")]
-	end: Option<Time>,
-
-	#[serde(default)]
-	#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")]
-	fast: Vec<(Time, Time)>,
-
-	#[serde(default)]
-	#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr, _)>")]
-	questions: Vec<(Time, Time, String)>,
-
-	metadata: Option<ProjectSourceMetadata>
-}
-
-#[serde_as]
-#[derive(Deserialize, Serialize)]
-struct ProjectSourceMetadata {
-	/// The duration of the source video.
-	#[serde_as(as = "DisplayFromStr")]
-	source_duration: Time,
-	/// The FPS of the source video.
-	#[serde_as(as = "DisplayFromStr")]
-	source_fps: Rational,
-	/// The time base of the source video.
-	#[serde_as(as = "DisplayFromStr")]
-	source_tbn: Rational,
-	/// The resolution of the source video.
-	source_res: Resolution,
-	/// The sample rate of the source audio.
-	source_sample_rate: u32
-}
-
-#[derive(Default, Deserialize, Serialize)]
-struct ProjectProgress {
-	#[serde(default)]
-	preprocessed: bool,
-
-	#[serde(default)]
-	asked_start_end: bool,
-
-	#[serde(default)]
-	asked_fast: bool,
-
-	#[serde(default)]
-	asked_questions: bool,
-
-	#[serde(default)]
-	rendered_assets: bool,
-
-	#[serde(default)]
-	rendered: bool,
-
-	#[serde(default)]
-	transcoded: BTreeSet<Resolution>
-}
-
 fn ask(question: impl Display) -> String {
 	let mut stdout = io::stdout().lock();
 	let mut stdin = io::stdin().lock();
diff --git a/src/render/mod.rs b/src/render/mod.rs
index 15fdf80889c8f81b5843bc155c607d899f8cdcec..cc34ed9359ac6139fcec80d28a7886c01812ceb8 100644
--- a/src/render/mod.rs
+++ b/src/render/mod.rs
@@ -7,10 +7,10 @@ use self::{
 };
 use crate::{
 	iotro::{intro, outro},
+	project::{Project, ProjectLecture, ProjectSourceMetadata, Resolution},
 	question::Question,
 	render::ffmpeg::{Ffmpeg, FfmpegInput},
-	time::{format_date, format_time, Time},
-	Project, ProjectLecture, ProjectSourceMetadata, Resolution
+	time::{format_date, format_time, Time}
 };
 use anyhow::{bail, Context};
 use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};