diff --git a/src/project.rs b/src/project.rs
new file mode 100644
index 0000000000000000000000000000000000000000..46ed082ad79abc6d096f8833a4dc9cd24e55a8d2
--- /dev/null
+++ b/src/project.rs
@@ -0,0 +1,162 @@
+use crate::{
+	iotro::Language,
+	render::ffmpeg::FfmpegOutputFormat,
+	time::{Date, Time}
+};
+use rational::Rational;
+use serde::{Deserialize, Serialize};
+use serde_with::{serde_as, DisplayFromStr};
+use std::{collections::BTreeSet, str::FromStr};
+
+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)]
+		pub(crate) 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 {
+			pub(crate) fn values() -> [Self; NUM_RESOLUTIONS] {
+				[$(Self::$res),+]
+			}
+
+			pub(crate) fn width(self) -> usize {
+				match self {
+					$(Self::$res => $width),+
+				}
+			}
+
+			pub(crate) fn height(self) -> usize {
+				match self {
+					$(Self::$res => $height),+
+				}
+			}
+
+			pub(crate) fn bitrate(self) -> u64 {
+				match self {
+					$(Self::$res => $bitrate),+
+				}
+			}
+
+			pub(crate) 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)]
+pub(crate) struct Project {
+	pub(crate) lecture: ProjectLecture,
+	pub(crate) source: ProjectSource,
+	pub(crate) progress: ProjectProgress
+}
+
+#[serde_as]
+#[derive(Deserialize, Serialize)]
+pub(crate) struct ProjectLecture {
+	pub(crate) course: String,
+	pub(crate) label: String,
+	pub(crate) docent: String,
+	#[serde_as(as = "DisplayFromStr")]
+	pub(crate) date: Date,
+	#[serde(default = "Default::default")]
+	#[serde_as(as = "DisplayFromStr")]
+	pub(crate) lang: Language<'static>
+}
+
+#[serde_as]
+#[derive(Deserialize, Serialize)]
+pub(crate) struct ProjectSource {
+	pub(crate) files: Vec<String>,
+	pub(crate) stereo: bool,
+
+	#[serde_as(as = "Option<DisplayFromStr>")]
+	pub(crate) start: Option<Time>,
+	#[serde_as(as = "Option<DisplayFromStr>")]
+	pub(crate) end: Option<Time>,
+
+	#[serde(default)]
+	#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")]
+	pub(crate) fast: Vec<(Time, Time)>,
+
+	#[serde(default)]
+	#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr, _)>")]
+	pub(crate) questions: Vec<(Time, Time, String)>,
+
+	pub(crate) metadata: Option<ProjectSourceMetadata>
+}
+
+#[serde_as]
+#[derive(Deserialize, Serialize)]
+pub(crate) struct ProjectSourceMetadata {
+	/// The duration of the source video.
+	#[serde_as(as = "DisplayFromStr")]
+	pub(crate) source_duration: Time,
+	/// The FPS of the source video.
+	#[serde_as(as = "DisplayFromStr")]
+	pub(crate) source_fps: Rational,
+	/// The time base of the source video.
+	#[serde_as(as = "DisplayFromStr")]
+	pub(crate) source_tbn: Rational,
+	/// The resolution of the source video.
+	pub(crate) source_res: Resolution,
+	/// The sample rate of the source audio.
+	pub(crate) source_sample_rate: u32
+}
+
+#[derive(Default, Deserialize, Serialize)]
+pub(crate) struct ProjectProgress {
+	#[serde(default)]
+	pub(crate) preprocessed: bool,
+
+	#[serde(default)]
+	pub(crate) asked_start_end: bool,
+
+	#[serde(default)]
+	pub(crate) asked_fast: bool,
+
+	#[serde(default)]
+	pub(crate) asked_questions: bool,
+
+	#[serde(default)]
+	pub(crate) rendered_assets: bool,
+
+	#[serde(default)]
+	pub(crate) rendered: bool,
+
+	#[serde(default)]
+	pub(crate) transcoded: BTreeSet<Resolution>
+}