From 8f15f51bce969db33e3c3a9daf037131e575d0e0 Mon Sep 17 00:00:00 2001 From: Marek Lenczewski Date: Tue, 7 Apr 2026 16:13:16 +0200 Subject: [PATCH] update --- backend/Dockerfile | 2 +- backend/{routes => api}/__init__.py | 0 backend/api/profile_controller.py | 13 +++ backend/{ => api}/schemas.py | 0 .../videos.py => api/video_controller.py} | 53 +++++------- backend/{services => base}/__init__.py | 0 backend/base/app.py | 36 ++++++++ backend/database/__init__.py | 0 backend/{ => database}/database.py | 0 backend/download/__init__.py | 0 .../download_service.py | 14 +++- backend/main.py | 59 -------------- backend/model/__init__.py | 0 backend/model/profile.py | 15 ++++ backend/model/profile_video.py | 10 +++ backend/model/video.py | 77 ++++++++++++++++++ backend/models.py | 30 ------- backend/notify/__init__.py | 0 backend/notify/notify_clients.py | 24 ++++++ .../__pycache__/__init__.cpython-312.pyc | Bin 120 -> 0 bytes .../routes/__pycache__/videos.cpython-312.pyc | Bin 8039 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 122 -> 0 bytes .../download_service.cpython-312.pyc | Bin 990 -> 0 bytes .../__pycache__/video_service.cpython-312.pyc | Bin 5287 -> 0 bytes backend/services/video_service.py | 69 ---------------- backend/stream/__init__.py | 0 .../{services => stream}/stream_service.py | 0 browser_extension/{ => api}/background.js | 0 browser_extension/{ => config}/popup.html | 0 browser_extension/{ => config}/popup.js | 0 browser_extension/manifest.json | 6 +- browser_extension/{ => tracking}/content.js | 0 32 files changed, 212 insertions(+), 196 deletions(-) rename backend/{routes => api}/__init__.py (100%) create mode 100644 backend/api/profile_controller.py rename backend/{ => api}/schemas.py (100%) rename backend/{routes/videos.py => api/video_controller.py} (68%) rename backend/{services => base}/__init__.py (100%) create mode 100644 backend/base/app.py create mode 100644 backend/database/__init__.py rename backend/{ => database}/database.py (100%) create mode 100644 backend/download/__init__.py rename backend/{services => download}/download_service.py (57%) delete mode 100644 backend/main.py create mode 100644 backend/model/__init__.py create mode 100644 backend/model/profile.py create mode 100644 backend/model/profile_video.py create mode 100644 backend/model/video.py delete mode 100644 backend/models.py create mode 100644 backend/notify/__init__.py create mode 100644 backend/notify/notify_clients.py delete mode 100644 backend/routes/__pycache__/__init__.cpython-312.pyc delete mode 100644 backend/routes/__pycache__/videos.cpython-312.pyc delete mode 100644 backend/services/__pycache__/__init__.cpython-312.pyc delete mode 100644 backend/services/__pycache__/download_service.cpython-312.pyc delete mode 100644 backend/services/__pycache__/video_service.cpython-312.pyc delete mode 100644 backend/services/video_service.py create mode 100644 backend/stream/__init__.py rename backend/{services => stream}/stream_service.py (100%) rename browser_extension/{ => api}/background.js (100%) rename browser_extension/{ => config}/popup.html (100%) rename browser_extension/{ => config}/popup.js (100%) rename browser_extension/{ => tracking}/content.js (100%) diff --git a/backend/Dockerfile b/backend/Dockerfile index d013b81..92c94c9 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,4 +10,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] +CMD ["uvicorn", "base.app:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/backend/routes/__init__.py b/backend/api/__init__.py similarity index 100% rename from backend/routes/__init__.py rename to backend/api/__init__.py diff --git a/backend/api/profile_controller.py b/backend/api/profile_controller.py new file mode 100644 index 0000000..3043391 --- /dev/null +++ b/backend/api/profile_controller.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from api.schemas import ProfileResponse +from database.database import get_db +from model.profile import Profile + +router = APIRouter(prefix="/profiles", tags=["profiles"]) + + +@router.get("", response_model=list[ProfileResponse]) +def get_profiles(db: Session = Depends(get_db)): + return Profile.get_all(db) diff --git a/backend/schemas.py b/backend/api/schemas.py similarity index 100% rename from backend/schemas.py rename to backend/api/schemas.py diff --git a/backend/routes/videos.py b/backend/api/video_controller.py similarity index 68% rename from backend/routes/videos.py rename to backend/api/video_controller.py index 6b483f2..cf2c393 100644 --- a/backend/routes/videos.py +++ b/backend/api/video_controller.py @@ -1,18 +1,17 @@ -import asyncio import threading from pathlib import Path from typing import Optional -from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import FileResponse, StreamingResponse from sqlalchemy.orm import Session -from database import get_db -from schemas import CleanupRequest, ProfileResponse, VideoCreate, VideoResponse -from services import video_service -from services.download_service import download_video -from services.stream_service import stream_video_live -from services.video_service import update_file_path +from api.schemas import CleanupRequest, VideoCreate, VideoResponse +from database.database import SessionLocal, get_db +from download.download_service import download_video +from model.video import Video +from notify.notify_clients import notify_clients +from stream.stream_service import stream_video_live router = APIRouter(prefix="/videos", tags=["videos"]) @@ -23,14 +22,14 @@ async def create_videos(videos_data: list[VideoCreate], db: Session = Depends(ge profile_ids = set() for video_data in reversed(videos_data): video_id_match = video_data.youtube_url.split("v=")[-1].split("&")[0] - video_service.delete_by_youtube_id(db, video_id_match) - video = video_service.create_video(db, video_data) + Video.delete_by_youtube_id(db, video_id_match) + data = video_data.model_dump(exclude={"profile_id"}) + video = Video.create_from_dict(db, data, video_data.profile_id) created_ids.append(video.id) profile_ids.add(video_data.profile_id or 1) - videos = [video_service.get_video(db, vid) for vid in created_ids] + videos = [Video.get_by_id(db, vid) for vid in created_ids] if profile_ids: - from main import notify_clients await notify_clients(list(profile_ids)) return [VideoResponse.from_model(v) for v in videos if v] @@ -38,25 +37,25 @@ async def create_videos(videos_data: list[VideoCreate], db: Session = Depends(ge @router.get("", response_model=list[VideoResponse]) def get_all_videos(profile_id: Optional[int] = Query(None), db: Session = Depends(get_db)): - videos = video_service.get_all_videos(db, profile_id=profile_id) + videos = Video.get_all(db, profile_id=profile_id) return [VideoResponse.from_model(v) for v in videos] @router.get("/downloaded", response_model=list[VideoResponse]) def get_downloaded_videos(profile_id: Optional[int] = Query(None), db: Session = Depends(get_db)): - videos = video_service.get_downloaded_videos(db, profile_id=profile_id) + videos = Video.get_downloaded(db, profile_id=profile_id) return [VideoResponse.from_model(v) for v in videos] @router.post("/cleanup") def cleanup_videos(request: CleanupRequest, db: Session = Depends(get_db)): - count = video_service.delete_not_downloaded(db, request.profile_id, request.exclude_ids or None) + count = Video.delete_not_downloaded(db, request.profile_id, request.exclude_ids or None) return {"deleted": count} @router.post("/{video_id}/download") def trigger_download(video_id: int, db: Session = Depends(get_db)): - video = video_service.get_video(db, video_id) + video = Video.get_by_id(db, video_id) if not video: raise HTTPException(status_code=404, detail="Video nicht gefunden") if video.file_path: @@ -69,7 +68,7 @@ def trigger_download(video_id: int, db: Session = Depends(get_db)): @router.get("/{video_id}/stream") def stream_video(video_id: int, db: Session = Depends(get_db)): - video = video_service.get_video(db, video_id) + video = Video.get_by_id(db, video_id) if not video: raise HTTPException(status_code=404, detail="Video nicht gefunden") @@ -78,9 +77,9 @@ def stream_video(video_id: int, db: Session = Depends(get_db)): output_path = f"/videos/{video_id}.mp4" yield from stream_video_live(video_id, video.youtube_url) if Path(output_path).exists(): - sdb = __import__("database").SessionLocal() + sdb = SessionLocal() try: - update_file_path(sdb, video_id, output_path) + Video.update_file_path(sdb, video_id, output_path) finally: sdb.close() @@ -95,7 +94,7 @@ def stream_video(video_id: int, db: Session = Depends(get_db)): @router.get("/{video_id}/file") def download_file(video_id: int, db: Session = Depends(get_db)): - video = video_service.get_video(db, video_id) + video = Video.get_by_id(db, video_id) if not video: raise HTTPException(status_code=404, detail="Video nicht gefunden") if not video.file_path: @@ -103,7 +102,7 @@ def download_file(video_id: int, db: Session = Depends(get_db)): path = Path(video.file_path) if not path.exists(): - video_service.update_file_path(db, video_id, None) + Video.update_file_path(db, video_id, None) raise HTTPException(status_code=404, detail="Video noch nicht heruntergeladen") return FileResponse(path, media_type="video/mp4", filename=f"{video.title}.mp4") @@ -111,20 +110,12 @@ def download_file(video_id: int, db: Session = Depends(get_db)): @router.delete("/{video_id}/file") def delete_server_file(video_id: int, db: Session = Depends(get_db)): - video = video_service.get_video(db, video_id) + video = Video.get_by_id(db, video_id) if not video: raise HTTPException(status_code=404, detail="Video nicht gefunden") if video.file_path: path = Path(video.file_path) if path.exists(): path.unlink() - video_service.update_file_path(db, video_id, None) + Video.update_file_path(db, video_id, None) return {"status": "deleted"} - - -profiles_router = APIRouter(prefix="/profiles", tags=["profiles"]) - - -@profiles_router.get("", response_model=list[ProfileResponse]) -def get_profiles(db: Session = Depends(get_db)): - return video_service.get_all_profiles(db) diff --git a/backend/services/__init__.py b/backend/base/__init__.py similarity index 100% rename from backend/services/__init__.py rename to backend/base/__init__.py diff --git a/backend/base/app.py b/backend/base/app.py new file mode 100644 index 0000000..9049966 --- /dev/null +++ b/backend/base/app.py @@ -0,0 +1,36 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from api.profile_controller import router as profiles_router +from api.video_controller import router as videos_router +from database.database import SessionLocal, create_tables +from model.profile import Profile +from notify.notify_clients import register_websocket + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(videos_router) +app.include_router(profiles_router) +register_websocket(app) + + +@app.on_event("startup") +def startup(): + create_tables() + db = SessionLocal() + if db.query(Profile).count() == 0: + db.add(Profile(name="Standard")) + db.commit() + db.close() + + +@app.get("/") +def root(): + return {"status": "running"} diff --git a/backend/database/__init__.py b/backend/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/database.py b/backend/database/database.py similarity index 100% rename from backend/database.py rename to backend/database/database.py diff --git a/backend/download/__init__.py b/backend/download/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/download_service.py b/backend/download/download_service.py similarity index 57% rename from backend/services/download_service.py rename to backend/download/download_service.py index b0dfa15..f8a049e 100644 --- a/backend/services/download_service.py +++ b/backend/download/download_service.py @@ -1,9 +1,11 @@ import subprocess +from pathlib import Path -from database import SessionLocal -from services.video_service import get_video, update_file_path +from database.database import SessionLocal +from model.video import Video VIDEOS_DIR = "/videos" +MIN_VALID_SIZE = 1024 * 100 # 100 KB def download_video(video_id: int, youtube_url: str): @@ -15,13 +17,19 @@ def download_video(video_id: int, youtube_url: str): "-f", "bestvideo[ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/best[ext=mp4]", "-o", output_path, "--merge-output-format", "mp4", + "--force-overwrites", + "--no-continue", youtube_url, ], check=True, ) + path = Path(output_path) + if not path.exists() or path.stat().st_size < MIN_VALID_SIZE: + return + db = SessionLocal() try: - update_file_path(db, video_id, output_path) + Video.update_file_path(db, video_id, output_path) finally: db.close() diff --git a/backend/main.py b/backend/main.py deleted file mode 100644 index 6d21d98..0000000 --- a/backend/main.py +++ /dev/null @@ -1,59 +0,0 @@ -from fastapi import FastAPI, WebSocket, WebSocketDisconnect -from fastapi.middleware.cors import CORSMiddleware - -from database import SessionLocal, create_tables -from models import Profile -from routes.videos import profiles_router, router as videos_router - -app = FastAPI() - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_methods=["*"], - allow_headers=["*"], -) - -app.include_router(videos_router) -app.include_router(profiles_router) - -# --- WebSocket --- - -connected_clients: set[WebSocket] = set() - - -@app.websocket("/ws") -async def websocket_endpoint(websocket: WebSocket): - await websocket.accept() - connected_clients.add(websocket) - try: - while True: - await websocket.receive_text() - except WebSocketDisconnect: - connected_clients.discard(websocket) - - -async def notify_clients(profile_ids: list[int]): - message = ",".join(str(pid) for pid in profile_ids) - for client in list(connected_clients): - try: - await client.send_text(message) - except Exception: - connected_clients.discard(client) - - -# --- Startup --- - -@app.on_event("startup") -def startup(): - create_tables() - db = SessionLocal() - if db.query(Profile).count() == 0: - db.add(Profile(name="Standard")) - db.commit() - db.close() - - -@app.get("/") -def root(): - return {"status": "running"} diff --git a/backend/model/__init__.py b/backend/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/model/profile.py b/backend/model/profile.py new file mode 100644 index 0000000..cd1c56d --- /dev/null +++ b/backend/model/profile.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import Session + +from database.database import Base + + +class Profile(Base): + __tablename__ = "profiles" + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String, nullable=False, unique=True) + + @classmethod + def get_all(cls, db: Session) -> list["Profile"]: + return db.query(cls).all() diff --git a/backend/model/profile_video.py b/backend/model/profile_video.py new file mode 100644 index 0000000..bb357ea --- /dev/null +++ b/backend/model/profile_video.py @@ -0,0 +1,10 @@ +from sqlalchemy import Column, ForeignKey, Integer, Table + +from database.database import Base + +video_profiles = Table( + "video_profiles", + Base.metadata, + Column("video_id", Integer, ForeignKey("videos.id", ondelete="CASCADE")), + Column("profile_id", Integer, ForeignKey("profiles.id", ondelete="CASCADE")), +) diff --git a/backend/model/video.py b/backend/model/video.py new file mode 100644 index 0000000..a78da47 --- /dev/null +++ b/backend/model/video.py @@ -0,0 +1,77 @@ +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import Session, relationship + +from database.database import Base +from model.profile import Profile +from model.profile_video import video_profiles + + +class Video(Base): + __tablename__ = "videos" + + id = Column(Integer, primary_key=True, autoincrement=True) + title = Column(String, nullable=False) + youtuber = Column(String, nullable=False) + thumbnail_url = Column(String, nullable=False) + youtube_url = Column(String, nullable=False) + file_path = Column(String, nullable=True) + profiles = relationship("Profile", secondary=video_profiles, backref="videos") + + @classmethod + def create_from_dict(cls, db: Session, data: dict, profile_id: int | None) -> "Video": + video = cls(**data) + if not profile_id: + profile_id = 1 + profile = db.query(Profile).filter(Profile.id == profile_id).first() + if profile: + video.profiles.append(profile) + db.add(video) + db.commit() + db.refresh(video) + return video + + @classmethod + def get_all(cls, db: Session, profile_id: int | None = None) -> list["Video"]: + query = db.query(cls) + if profile_id: + query = query.filter(cls.profiles.any(Profile.id == profile_id)) + return query.order_by(cls.id.desc()).all() + + @classmethod + def get_downloaded(cls, db: Session, profile_id: int | None = None) -> list["Video"]: + query = db.query(cls).filter(cls.file_path.isnot(None)) + if profile_id: + query = query.filter(cls.profiles.any(Profile.id == profile_id)) + return query.order_by(cls.id.desc()).all() + + @classmethod + def get_by_id(cls, db: Session, video_id: int) -> "Video | None": + return db.query(cls).filter(cls.id == video_id).first() + + @classmethod + def delete_by_youtube_id(cls, db: Session, youtube_id: str): + db.query(cls).filter(cls.youtube_url.contains(youtube_id)).delete(synchronize_session=False) + db.commit() + + @classmethod + def update_file_path(cls, db: Session, video_id: int, path: str | None): + video = cls.get_by_id(db, video_id) + if video: + video.file_path = path + db.commit() + + @classmethod + def delete_not_downloaded(cls, db: Session, profile_id: int, exclude_ids: list[int] | None = None) -> int: + query = db.query(cls).filter( + cls.profiles.any(Profile.id == profile_id), + ) + if exclude_ids: + query = query.filter(cls.id.notin_(exclude_ids)) + videos = query.all() + video_ids = [v.id for v in videos] + if not video_ids: + return 0 + db.execute(video_profiles.delete().where(video_profiles.c.video_id.in_(video_ids))) + db.query(cls).filter(cls.id.in_(video_ids)).delete(synchronize_session=False) + db.commit() + return len(video_ids) diff --git a/backend/models.py b/backend/models.py deleted file mode 100644 index e61527a..0000000 --- a/backend/models.py +++ /dev/null @@ -1,30 +0,0 @@ -from sqlalchemy import Column, ForeignKey, Integer, String, Table -from sqlalchemy.orm import relationship - -from database import Base - -video_profiles = Table( - "video_profiles", - Base.metadata, - Column("video_id", Integer, ForeignKey("videos.id", ondelete="CASCADE")), - Column("profile_id", Integer, ForeignKey("profiles.id", ondelete="CASCADE")), -) - - -class Profile(Base): - __tablename__ = "profiles" - - id = Column(Integer, primary_key=True, autoincrement=True) - name = Column(String, nullable=False, unique=True) - - -class Video(Base): - __tablename__ = "videos" - - id = Column(Integer, primary_key=True, autoincrement=True) - title = Column(String, nullable=False) - youtuber = Column(String, nullable=False) - thumbnail_url = Column(String, nullable=False) - youtube_url = Column(String, nullable=False) - file_path = Column(String, nullable=True) - profiles = relationship("Profile", secondary=video_profiles, backref="videos") diff --git a/backend/notify/__init__.py b/backend/notify/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/notify/notify_clients.py b/backend/notify/notify_clients.py new file mode 100644 index 0000000..b426e4f --- /dev/null +++ b/backend/notify/notify_clients.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI, WebSocket, WebSocketDisconnect + +connected_clients: set[WebSocket] = set() + + +async def notify_clients(profile_ids: list[int]): + message = ",".join(str(pid) for pid in profile_ids) + for client in list(connected_clients): + try: + await client.send_text(message) + except Exception: + connected_clients.discard(client) + + +def register_websocket(app: FastAPI): + @app.websocket("/ws") + async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + connected_clients.add(websocket) + try: + while True: + await websocket.receive_text() + except WebSocketDisconnect: + connected_clients.discard(websocket) diff --git a/backend/routes/__pycache__/__init__.cpython-312.pyc b/backend/routes/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index a265420935b6a216f685fb5089bcaefb9e316181..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120 zcmX@j%ge<81h0}WXM*U*AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxdC9a=XP@rFwUs{q{ ttREkrnU`4-AFo$Xd5gm)H$SB`C)KWq6{wC8h>JmtkIamWj77{q766mx81VoA diff --git a/backend/routes/__pycache__/videos.cpython-312.pyc b/backend/routes/__pycache__/videos.cpython-312.pyc deleted file mode 100644 index 550176aaedda70faa57d1deb297b179d822f1674..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8039 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 diff --git a/backend/services/__pycache__/__init__.cpython-312.pyc b/backend/services/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index e6c80eaa6a4c6046b249fceaf832f15f43d24083..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 122 zcmX@j%ge<81h0}WXM*U*AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxdC8?iSP@rF&T2z*q voLa0OAD@|*SrQ+wS5SG2!zMRBr8Fniu80+=k`aiDL5z>gjEsy$%s>_Z)QcIl diff --git a/backend/services/__pycache__/download_service.cpython-312.pyc b/backend/services/__pycache__/download_service.cpython-312.pyc deleted file mode 100644 index 5d04e6b6322726e209ad9ef05802513142f2a775..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 990 zcmah{O=uHA6rS0?Y&J=46BD%ue<-M-q!H|~EmrEGSR&Xeno`4NXVRwbW|!IBw27%i z5L86$tpyQstkk<_&t3}lP}YJ4^`NIfD@aeyY_jp_gLyySd-LY~%%@mP0hE4epUaOq z0KPFJAfc-qj!|U;Ab_9_@=S6%G`Ktmna=CHA>;)^%!||(@)8np2uWBj2`Kbb$cJk% zE&pVxa%t#AXR&RU&B~0asd}38!i(4`RLcmPUcYN0)xm{QS;qxSb(ZR(tnaZs*_VYf z@**|Ub=)E@xJ1`z!98Ma@bnz+Qnmq{p5qNIy8$TY-U@4+!(x0|4%!Oe^grTVoZwjF;!^LW)6r|yOMs%9drJs4N3+QKmN zt1c>=9sj7hkY!LWnr1QU$xOz;WD#fRd@a|>luTl%j>pqyCYMJKBB6Fppn<^@FkrAur_4Po%Q<9)}+1PD^vL@J1o?D-3!Pe#0;MV9){7UoY zccJfD?n&+-dU7w?zZ0LMe0_K?I@G$e7d_RENMWVj2Tmk@4}?YOCCXD)fnFp;a{2{K4cNRNGyvNjJP i`~n7kfD2#2#6c|8RN7J;CN^i=fQl{qhzW9rhW-U;r}sMm diff --git a/backend/services/__pycache__/video_service.cpython-312.pyc b/backend/services/__pycache__/video_service.cpython-312.pyc deleted file mode 100644 index b792b3fd5d7fc8e03283a2330fe4eda446726477..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5287 zcmcgvT}&I<6`t{ofAHAG4kp=!B|kt2Zum)>upwv@cAG$eH0)M{-3{A~MLa{WYi#z8 z9ZF;>qNXhx*`ZLc)d`*X!n>t;h7}UtApbSA1cuqCjlgc#gZx$(+o`xe0!R z=aII>Z4<(XFd>eJ6ZR21eFeE~7HGA2oAa@%u`~FO8oT$QgUaPHHn^%wL*gRIG|or7>e? zQh5%}UtOAER7TvgVi)6=Mz%MVfL7ktwnz|%xR8C&a2ISEj>-rD<;RhDT2{V9O|Nkw zFF_?3?un$V#KUrWVv?Zd4f{uFg-jC>X$Jr$r4Yjwl@0q?l&C4g#eiZsMkgngglq_- zvTQga$%%<*%5V~8j40}O$U)F11U=H+NTl$wQz#iuCdC2sdujXU?sgE^t}5h8G@_{O zCguvSZIjc6CqfY!rW0g0e27R;e*@htmk-qD0!><=X?7qlRp+F=nzVPdWp#H}+N(>Q zv!|bV0`unC3yk3%+?@&Q+(m>Xt4)aW&;d$1i%X_0Dx%KZj)hwf^g8kr=o( zFqeKJR_7)E?Dsd}blf1-z!sOm|E4a}Xu1p+D>wkIXDwB9G6oNHBGR3)#fnbC)^c2F zbqu9%YcUoZ%lIi8Ns(dc5PZ2ql%2p9vv7{G-7TCe7AV`cKr%=($yTnz)(mlL{9wpU zPz;4B*?|MqoM~UlU9;;>oBC6?G4=BX#(R@3N8# z!!i?EDmehmo$#n5&;hnQzJ=R_rXA|j>-u&7z?>b-zi@K??M6V0RD}A|BNgbX#DT^VDme`gD*=nLt=VKlepoEBonF8XhJm{P;DqsQ@jFNVdiNEdW(62 z<{}8z)P-3Z8t;?m7F-%Dl_HP%&TeOKHQ-xxD62ONCMpAggZqcPywV zX1{{}{C{Ki8&xAh#zxvQf|cgWa)ebyVo}3niq{b4HE8D-)h=)`5l?dq_K58=WbzLo z!{Oz?s+#)PcGE9%GY*({sQfe!0n{q56*E^13y^J%VJT*D0Z3uIqb*j$sL~jFtwb{_ zhULe$mT|B@-#*g#P{exavyztC7vI1ZFLe^Dj4j(92y9<^!hNNaF^Xx8=TZ%fSNhK( zup1B0jD3m=iGu_kyq6q?KD1+7NV7`zh6AdAXd+BgJ>IdDNE)IW7T}}SCSt`NoG zBLtI>A>dl{c|(XRiIB6bF73i^u)sAOG&89Nf5oU^$yimpKh>)_Ir+5W7*>8TXVSNaRL zp_@O+RW@psje2EMuChg|Y#$z=?i2Ani@?6p zx!k$(#_}7h6?*M=b@50}?9#-p$LXxt^+X&l#PsWDpGm&C=^H1r;;aAo%F6`~sAz{j z?5gTHDn`n&i!qMIxjrENyEwtS;4O#d;dO&C;~W@TnigN8^!kWn#K5NhS#XkKv%VdDSdNu>zq;G@L4~ z8&wHnh+dr$h$%yiN7WR)?vR5xgK5bSfJJjHO)wNr^ne5-rg;QvM+~+>%mM&BF`jLmh&$=l2IQUrkQ!QPw;p6!?`Nabp9FCt1nd9p4 z`uVjVZc^Tc%f|bcV1F3a8#Zy+aEp9!X&1^0uAZX9x{Bh0&wXCL1~-IZ{RkZ%oqyc$ m`GxEchP9qy%E*q&n>fF*m**Q$3Jjl|Up;X5!@na*RrGJ({-*E% diff --git a/backend/services/video_service.py b/backend/services/video_service.py deleted file mode 100644 index 3734fb8..0000000 --- a/backend/services/video_service.py +++ /dev/null @@ -1,69 +0,0 @@ -from sqlalchemy.orm import Session - -from models import Profile, Video, video_profiles -from schemas import VideoCreate - - -def create_video(db: Session, video_data: VideoCreate) -> Video: - profile_id = video_data.profile_id - data = video_data.model_dump(exclude={"profile_id"}) - video = Video(**data) - if not profile_id: - profile_id = 1 - profile = db.query(Profile).filter(Profile.id == profile_id).first() - if profile: - video.profiles.append(profile) - db.add(video) - db.commit() - db.refresh(video) - return video - - -def get_all_videos(db: Session, profile_id: int | None = None) -> list[Video]: - query = db.query(Video) - if profile_id: - query = query.filter(Video.profiles.any(Profile.id == profile_id)) - return query.order_by(Video.id.desc()).all() - - -def get_downloaded_videos(db: Session, profile_id: int | None = None) -> list[Video]: - query = db.query(Video).filter(Video.file_path.isnot(None)) - if profile_id: - query = query.filter(Video.profiles.any(Profile.id == profile_id)) - return query.order_by(Video.id.desc()).all() - - -def get_video(db: Session, video_id: int) -> Video | None: - return db.query(Video).filter(Video.id == video_id).first() - - -def delete_by_youtube_id(db: Session, youtube_id: str): - db.query(Video).filter(Video.youtube_url.contains(youtube_id)).delete(synchronize_session=False) - db.commit() - - -def update_file_path(db: Session, video_id: int, path: str): - video = get_video(db, video_id) - if video: - video.file_path = path - db.commit() - - -def delete_not_downloaded(db: Session, profile_id: int, exclude_ids: list[int] | None = None) -> int: - query = db.query(Video).filter( - Video.profiles.any(Profile.id == profile_id), - ) - if exclude_ids: - query = query.filter(Video.id.notin_(exclude_ids)) - videos = query.all() - video_ids = [v.id for v in videos] - if not video_ids: - return 0 - db.execute(video_profiles.delete().where(video_profiles.c.video_id.in_(video_ids))) - db.query(Video).filter(Video.id.in_(video_ids)).delete(synchronize_session=False) - db.commit() - return len(video_ids) - - -def get_all_profiles(db: Session) -> list[Profile]: - return db.query(Profile).all() diff --git a/backend/stream/__init__.py b/backend/stream/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/stream_service.py b/backend/stream/stream_service.py similarity index 100% rename from backend/services/stream_service.py rename to backend/stream/stream_service.py diff --git a/browser_extension/background.js b/browser_extension/api/background.js similarity index 100% rename from browser_extension/background.js rename to browser_extension/api/background.js diff --git a/browser_extension/popup.html b/browser_extension/config/popup.html similarity index 100% rename from browser_extension/popup.html rename to browser_extension/config/popup.html diff --git a/browser_extension/popup.js b/browser_extension/config/popup.js similarity index 100% rename from browser_extension/popup.js rename to browser_extension/config/popup.js diff --git a/browser_extension/manifest.json b/browser_extension/manifest.json index 8c7995c..96ff6c5 100644 --- a/browser_extension/manifest.json +++ b/browser_extension/manifest.json @@ -11,14 +11,14 @@ "content_scripts": [ { "matches": ["*://www.youtube.com/*"], - "js": ["content.js"] + "js": ["tracking/content.js"] } ], "background": { - "scripts": ["background.js"] + "scripts": ["api/background.js"] }, "browser_action": { - "default_popup": "popup.html", + "default_popup": "config/popup.html", "default_title": "Profil auswählen" } } diff --git a/browser_extension/content.js b/browser_extension/tracking/content.js similarity index 100% rename from browser_extension/content.js rename to browser_extension/tracking/content.js