diff --git a/src/videoag_common/database/drift_detector.py b/src/videoag_common/database/drift_detector.py
index 059af8e341391bcac8266be7b9663e517707e007..4025e774e65045241cf9781e4fefd2c6559a9461 100644
--- a/src/videoag_common/database/drift_detector.py
+++ b/src/videoag_common/database/drift_detector.py
@@ -96,6 +96,10 @@ def _check_constraint_equal(actual_constraint: Constraint, schema_constraint: Co
             and actual_constraint.name != schema_constraint.name):
         return False
     
+    if actual_constraint.deferrable != schema_constraint.deferrable:
+        # TODO add to to_string
+        return False
+    
     if not isinstance(schema_constraint, ColumnCollectionConstraint):
         print(f"Unknown type of constraint {schema_constraint}. Can't compare")
         return False
@@ -113,13 +117,19 @@ def _check_constraint_equal(actual_constraint: Constraint, schema_constraint: Co
             if not isinstance(actual_constraint, CheckConstraint):
                 return False
             
-            # We try our best to check if they are equal
+            # We try our best to check if they are equal, but with things like 'x IN ()' it breaks (because the db
+            # transforms the statements)
             if schema_constraint.sqltext == actual_constraint.sqltext:
                 return True
             # Sometimes the db returns it without correct brackets. Just strip them
             if str(schema_constraint.sqltext).strip("()") == str(actual_constraint.sqltext).strip("()"):
                 return True
-            return False
+            
+            print(f"Warning: Can't compare schema clause of constraint '{schema_constraint.name}'\n"
+                  f"    {schema_constraint.sqltext}\n"
+                  f"  with clause in database\n"
+                  f"    {actual_constraint.sqltext}")
+            return True
         case ForeignKeyConstraint():
             assert isinstance(schema_constraint, ForeignKeyConstraint)  # For pycharm
             
diff --git a/src/videoag_common/media_process/__init__.py b/src/videoag_common/media_process/__init__.py
index d45dacf16076a7a593e208846b819ce9caaef93d..1d4cff64f5fe3e0fbdecac0c41f1d516e74a0526 100644
--- a/src/videoag_common/media_process/__init__.py
+++ b/src/videoag_common/media_process/__init__.py
@@ -5,4 +5,4 @@ from .basic_targets import (
     SourceFileTargetProducer,
     DownscaleVideoTarget,
 )
-from .media_process import MediaProcess, ApiMediaProcessField
+from .media_process import MediaProcess, ApiMediaProcessField, get_permanent_lecture_dir
diff --git a/src/videoag_common/media_process/media_process.py b/src/videoag_common/media_process/media_process.py
index 88ed5aa590696baaacf15bb0824a0c33c2ffb104..1909220352796ffa4c11504a620998a9a9e354a7 100644
--- a/src/videoag_common/media_process/media_process.py
+++ b/src/videoag_common/media_process/media_process.py
@@ -3,12 +3,29 @@ from collections.abc import Iterable
 from videoag_common.miscellaneous import *
 from videoag_common.database import *
 from videoag_common.api_object import ApiSimpleColumnField, FieldContext
+import videoag_common
 from .target import TargetProducer, TARGET_ID_PATTERN, TARGET_ID_MAX_LENGTH
 
 #
 # Important: Json default values should be avoided in case the default value is changed and an old process relied on the default
 #
 
+_PERMANENT_MEDIA_DIR_NAME = videoag_common.config["PERMANENT_MEDIA_DIR"]
+if _PERMANENT_MEDIA_DIR_NAME.endswith("/"):
+    raise ValueError("PERMANENT_MEDIA_DIR must NOT end with /")
+
+
+# noinspection PyUnresolvedReferences
+def get_permanent_lecture_dir(lecture: "Lecture") -> str:
+    """
+    Returns the db path for the directory of the given lecture. Does NOT end with a /
+    """
+    return (
+        f"{_PERMANENT_MEDIA_DIR_NAME}/"
+        f"course-{lecture.course.id}.{lecture.course.handle}/"
+        f"lecture-{lecture.id}.{lecture.time.strftime("%y%m%d")}"
+    )
+
 
 class MediaProcess:
     
diff --git a/src/videoag_common/miscellaneous/json.py b/src/videoag_common/miscellaneous/json.py
index 8895662ae8a52f46ea284f37327a5be97b565a8a..23f66fcce585f7ed4b01c460331bfe688ce0c9ea 100644
--- a/src/videoag_common/miscellaneous/json.py
+++ b/src/videoag_common/miscellaneous/json.py
@@ -186,7 +186,7 @@ def replace_json_arguments(data: JsonTypes, argument_data: JsonTypes) -> JsonTyp
     if not isinstance(data, str):
         raise TypeError(f"Unknown json type: {type(data)}")
     
-    def _sub_match(match):
+    def _get_match_value(match, force_string: bool = True):
         path = match.group(1)
         val = argument_data
         for path_ele in path.split("."):
@@ -194,7 +194,16 @@ def replace_json_arguments(data: JsonTypes, argument_data: JsonTypes) -> JsonTyp
                 raise ValueError(f"Can't replace with path '{path}' because element '{path_ele}' is not present "
                                  f"(or parent has bad type) in argument data")
             val = val[path_ele]
+        if force_string:
+            if isinstance(val, str) or isinstance(val, int):
+                val = str(val)
+            else:
+                raise ValueError(f"Can't replace with path '{path}' because result is not a str or int")
         return val
     
-    return _JSON_ARGUMENT_PATTERN.sub(_sub_match, data)
+    full_match = _JSON_ARGUMENT_PATTERN.fullmatch(data)
+    if full_match is not None:
+        return _get_match_value(full_match, force_string=False)
+    
+    return _JSON_ARGUMENT_PATTERN.sub(_get_match_value, data)
 
diff --git a/src/videoag_common/objects/course.py b/src/videoag_common/objects/course.py
index 0117ae3d798936ff9ae036cd0de1bebe74c52b95..09b7fb05d9c35a41ec7b4dd96475bf455f33dc96 100644
--- a/src/videoag_common/objects/course.py
+++ b/src/videoag_common/objects/course.py
@@ -172,7 +172,7 @@ class Lecture(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject,
         from .medium import TargetMedium
         return sql.and_(
             TargetMedium.lecture_id == Lecture.id,
-            TargetMedium.is_active(from_lecture=True)
+            TargetMedium.is_active(usable_check=False, from_lecture=True)
         )
     
     @staticmethod
diff --git a/src/videoag_common/objects/medium.py b/src/videoag_common/objects/medium.py
index 497bdd0db03697828ac3f12ea0bf8a331e56afdc..9e1160fed89917b5af63600f834af8f74af03149 100644
--- a/src/videoag_common/objects/medium.py
+++ b/src/videoag_common/objects/medium.py
@@ -1,6 +1,8 @@
 from datetime import datetime
 from enum import Enum
+from pathlib import Path
 
+import videoag_common
 from videoag_common.database import *
 from videoag_common.miscellaneous import *
 from videoag_common.api_object import *
@@ -16,6 +18,11 @@ LOAD_TARGET_MEDIUM_PUBLISH_MEDIUM = RelationshipLoadMarker()
 LOAD_TARGET_MEDIUM_LECTURE = RelationshipLoadMarker()
 
 
+_API_BASE_URL = videoag_common.config["API_BASE_URL"]
+if _API_BASE_URL.endswith("/"):
+    raise ValueError("API_BASE_URL must NOT end with /")
+
+
 # A file which was uploaded and needs to be assigned to a lecture, to be converted to a SourceFileTargetMedium
 class SourceMedium(DeletableApiObject, Base):
     __table_args__ = (
@@ -116,6 +123,42 @@ class TargetMedium(DeletableApiObject, Base):
         "polymorphic_on": "type",
         "with_polymorphic": "*"  # Always load all attributes for all types
     }
+    # This isn't pretty. With a joined table inheritance it might be a bit nicer, but we couldn't check is_produced
+    # Also with a joined table inheritance we would have much more columns (since here we can reuse columns for different types)
+    __table_args__ = (
+        CheckConstraint(
+            f"type NOT IN ('plain_video', 'plain_audio') OR NOT is_produced OR duration_sec IS NOT NULL",
+            name="check_duration_sec_not_null"
+        ),
+        CheckConstraint(
+            f"type NOT IN ('plain_video', 'plain_audio', 'thumbnail') OR NOT is_produced OR file_path IS NOT NULL",
+            name="check_file_path_not_null"
+        ),
+        CheckConstraint(
+            f"type NOT IN ('plain_video', 'plain_audio') OR NOT is_produced OR audio_sample_rate IS NOT NULL",
+            name="check_audio_sample_rate_not_null"
+        ),
+        CheckConstraint(
+            f"type NOT IN ('plain_video', 'plain_audio') OR NOT is_produced OR audio_channel_count IS NOT NULL",
+            name="check_audio_channel_count_not_null"
+        ),
+        CheckConstraint(
+            f"type NOT IN ('plain_video') OR NOT is_produced OR video_vertical_resolution IS NOT NULL",
+            name="check_video_vertical_resolution_not_null"
+        ),
+        CheckConstraint(
+            f"type NOT IN ('plain_video') OR NOT is_produced OR video_horizontal_resolution IS NOT NULL",
+            name="check_video_horizontal_resolution_not_null"
+        ),
+        CheckConstraint(
+            f"type NOT IN ('plain_video') OR NOT is_produced OR video_frame_rate_numerator IS NOT NULL",
+            name="check_video_frame_rate_numerator_not_null"
+        ),
+        CheckConstraint(
+            f"type NOT IN ('plain_video') OR NOT is_produced OR video_frame_rate_denominator IS NOT NULL",
+            name="check_video_frame_rate_denominator_not_null"
+        ),
+    )
     
     # Note that these four may NOT be unique. E.g. if one output is generated twice (because another output of the same
     # producer was deleted, etc.)
@@ -210,13 +253,11 @@ class FileMedium(ApiObject):
         data_notes="URL to the mediums's file (Maybe with a redirect)"
     )
     def url(self) -> str:
-        # This is not too nice and only works from the API, but we need to access the API config
-        import api
-        return f"{api.config['API_BASE_URL']}/resources/target_medium/{self.id}"
+        return f"{_API_BASE_URL}/resources/target_medium/{self.id}"
     
     def get_default_file_path_no_ending(self):
-        # TODO path
-        return f"{self.process_target_id}.{self.id}"
+        assert isinstance(self, TargetMedium)
+        return f"{get_permanent_lecture_dir(self.lecture)}/target-{self.id}.{self.process_target_id}"
 
 
 class SingleAudioContainingMedium(ApiObject):