From 8ecef00d0aee5b31bf0aa1e1acc49cf77c43ba4b Mon Sep 17 00:00:00 2001 From: Marek Lenczewski Date: Mon, 6 Apr 2026 10:42:29 +0200 Subject: [PATCH] update --- .../reports/problems/problems-report.html | 2 +- .../java/com/youtubeapp/data/ApiClient.kt | 2 +- .../youtubeapp/ui/navigation/AppNavigation.kt | 2 +- .../ui/screens/VideoPlayerScreen.kt | 47 +++++-- .../youtubeapp/ui/viewmodel/VideoViewModel.kt | 6 +- .../__pycache__/__init__.cpython-312.pyc | Bin 120 -> 120 bytes .../routes/__pycache__/videos.cpython-312.pyc | Bin 7998 -> 8062 bytes backend/routes/videos.py | 3 +- .../__pycache__/__init__.cpython-312.pyc | Bin 122 -> 122 bytes .../download_service.cpython-312.pyc | Bin 990 -> 990 bytes .../__pycache__/video_service.cpython-312.pyc | Bin 5273 -> 5287 bytes backend/services/stream_service.py | 116 ++++++++++++------ backend/services/video_service.py | 9 +- browser_extension/background.js | 2 +- 14 files changed, 128 insertions(+), 61 deletions(-) diff --git a/app/build/reports/problems/problems-report.html b/app/build/reports/problems/problems-report.html index 5febd65..c91dc01 100644 --- a/app/build/reports/problems/problems-report.html +++ b/app/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/app/frontend/src/main/java/com/youtubeapp/data/ApiClient.kt b/app/frontend/src/main/java/com/youtubeapp/data/ApiClient.kt index 65a051a..8427460 100644 --- a/app/frontend/src/main/java/com/youtubeapp/data/ApiClient.kt +++ b/app/frontend/src/main/java/com/youtubeapp/data/ApiClient.kt @@ -5,7 +5,7 @@ import retrofit2.converter.gson.GsonConverterFactory object ApiClient { // Server-IP hier anpassen - const val BASE_URL = "http://192.168.178.92:8000/" + const val BASE_URL = "http://marha.local:8000/" val api: VideoApi by lazy { Retrofit.Builder() diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt b/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt index 1148d47..92d090a 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt @@ -157,7 +157,7 @@ fun AppNavigation() { arguments = listOf(navArgument("videoId") { type = NavType.IntType }) ) { backStackEntry -> val videoId = backStackEntry.arguments?.getInt("videoId") ?: return@composable - VideoPlayerScreen(videoId = videoId, viewModel = viewModel) + VideoPlayerScreen(videoId = videoId, viewModel = viewModel, onBack = { navController.popBackStack() }) } } } diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt b/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt index 622e2f6..15a4f7e 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt @@ -2,12 +2,21 @@ package com.youtubeapp.ui.screens import android.app.Activity import android.view.LayoutInflater +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -20,7 +29,7 @@ import com.youtubeapp.ui.viewmodel.VideoViewModel @Composable @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) -fun VideoPlayerScreen(videoId: Int, viewModel: VideoViewModel) { +fun VideoPlayerScreen(videoId: Int, viewModel: VideoViewModel, onBack: () -> Unit) { val context = LocalContext.current val playbackUri = viewModel.getPlaybackUri(videoId) @@ -49,15 +58,29 @@ fun VideoPlayerScreen(videoId: Int, viewModel: VideoViewModel) { } } - AndroidView( - factory = { ctx -> - val view = LayoutInflater.from(ctx).inflate(R.layout.player_view, null) as PlayerView - view.player = exoPlayer - view - }, - update = { view -> - view.player = exoPlayer - }, - modifier = Modifier.fillMaxSize() - ) + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + factory = { ctx -> + val view = LayoutInflater.from(ctx).inflate(R.layout.player_view, null) as PlayerView + view.player = exoPlayer + view + }, + update = { view -> + view.player = exoPlayer + }, + modifier = Modifier.fillMaxSize() + ) + IconButton( + onClick = onBack, + modifier = Modifier + .align(Alignment.TopStart) + .padding(16.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Zurück", + tint = Color.White + ) + } + } } diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt b/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt index 6e8bf32..be37466 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt @@ -195,6 +195,10 @@ class VideoViewModel : ViewModel() { fun getPlaybackUri(videoId: Int): String { val localFile = localStorage?.getLocalFile(videoId) - return localFile?.toURI()?.toString() ?: repository.getStreamUrl(videoId) + return if (localFile != null) { + android.net.Uri.fromFile(localFile).toString() + } else { + repository.getStreamUrl(videoId) + } } } diff --git a/backend/routes/__pycache__/__init__.cpython-312.pyc b/backend/routes/__pycache__/__init__.cpython-312.pyc index 69808bfff03637b772378763588225c7b95905fc..a265420935b6a216f685fb5089bcaefb9e316181 100644 GIT binary patch delta 17 Xcmb=Z;6BaE%f$c$uaYlM^G%qg~0}xacUd}Ao$op1=v1;;fkvhio$yK83ncNsB=kTRW?iWjCRw-ee ztivWhxmS{R@}+`G&Y6R}N5FA`lmMPfiffoU9|EBhMjvokQ&+huVUo6&4#p zu5ei0;gep$e8D8_3SamIj_}Q?677tP*^}Q&s&VB5brynjX99KWPxg_z#aayHHg49I zPG{6i2QrI57TjV>NzF+uNi71Yzr|lrl%JNFlUf{KlwS%IEGnM7TSkhpZSysmQ;do{ ijG>G(oIfyt=mp`Q!1QNk1}23MOdz2Ta+43r)dB!;%vjq1 delta 314 zcmexox6h9EG%qg~0}y<@cqvnFBkx-g#$S_vi_|e@Os*1L&m_S(IfpN0a=%!rE^i4d zNF4~IFs87sVP4Gy;WaUq@IqJ&3^mLtY&A?-e3SjfMFrXCGOgiQ%?yzXX3*rETqPBxSX zo%~2tWb+z{21dr5$+}W%Tm?WG5H8LH%4$w-lX}2f0^~Mq&X!JR)XV@fi$Dh4VoOQQ zNi9h&0;#>lUr>|}vaLA2D8CdaSX46kw~Q2H`(`oOQ;dq-jG>G(oIfyt=mp`Q!1QNk R1}23MOdz2TGLsqQYXR{TQbzy) diff --git a/backend/routes/videos.py b/backend/routes/videos.py index 4a294ac..72f0da2 100644 --- a/backend/routes/videos.py +++ b/backend/routes/videos.py @@ -104,7 +104,8 @@ def download_file(video_id: int, db: Session = Depends(get_db)): path = Path(video.file_path) if not path.exists(): - raise HTTPException(status_code=404, detail="Videodatei nicht gefunden") + video_service.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") diff --git a/backend/services/__pycache__/__init__.cpython-312.pyc b/backend/services/__pycache__/__init__.cpython-312.pyc index 763eb5b46ed9ac417ddefa9ac4919e7a68ac8a8c..e6c80eaa6a4c6046b249fceaf832f15f43d24083 100644 GIT binary patch delta 17 Xcmb=b;y%sG%f$c$uaYlMow?1d0<>f>C@#l!Zd$f~Xr4T{NY0rDMjH+?iS-5L~Hj zgyfp2EKE#Da0%(goyM(fnre#k4_uh|hzsM!bFQG$CjI8jnKR${&UfxS)7N$7ouY(2 z@Y}f6Ft;|96+-Uw=VUS|o1S@TjXWX?^1Rpf8q$Kd_KWDG%QxwUXIS~BY0;c%=)?`s zy9KLa&`$(t_^9+Y+yy}Z{`M*l$-@(o)OM=lVR&1OEf<%HkE;z;YpR*Lnpvx^&%K!2 zxLzMP-%!Vz>UdopZ>ZUO6CVTWc2HSXm(*r3T@R+8PQMRkYV)!~v^elGA3_2l1>j2g z(kzqlHp$!k{R3EIO90*7-7Jk2>NIt7dCSV(F%4R_*#IOv_zi!7Xtme=RkGLv2|dd8 zvb!NMTJiu zwRnnu4o(q`Uk+_#g&JRr8kR)%b^zVWf*2phm%RYHV}`Ji7?B-?bRW-#M_|ImutFmL zjmDoudQa_xf?cAaR-$%;TGXMrsoC6Yx#Co&aMx@E$`0{wkxL+RH0oRtG9}T9>F|sR zPWhiWb`rCl$d2oQb?odTR+%yK!2j1@b{d`^;a+73^d3_((&8{A^hB!%xbGJKT|X?w zEaimzcp7P;;!x(t$`b&tuUe#xx}+csb0w@rrLJVrk{)LmKsr42|>RNiWZ`p6d`9U}tmr zvdhG<_BzxIWdiV*W(3u@?g_*eX#LHu>s@zx=V)r4rmHmlr1xpx=m4$ae% zDjk`l`!5DQhv|Y9yOz0>nYWTvD|xT;qm`~))_u|%g_7Jrh#}MiNNu!mj_c@B(QLM_ z!km%@VB0ot5(mwCqXWDK;h8zoQh96MCdX_DI2QlEoUBuq86Wk>q~VncMPD`GNl0F7 zfYy416ha$9JHWDbuY&80G=!SOm*5m>5y$JE?X|&oj1}EDg(#2XRxJ;r-3?Hx1wh`g z$huR^&z|vIR^%JOPl+R;9=O4kP=th+Q5O%wsm8UC@VC$=u$Kynui=ET10tO_tPvoz z&+-r1n5?OyybEluPV4_@@E(jN07|GVD7i=}S{`>teU@EPitmK|4I&!Z49o^19oqRV z5OC1)s|x&z6wFr tQ8a`7r;Bbm&vx?zVkX|22$R_K(eE{|JnB}= 2: + return lines[0], lines[1] + elif len(lines) == 1: + return lines[0], None + return None, None def stream_video_live(video_id: int, youtube_url: str): output_path = f"{VIDEOS_DIR}/{video_id}.mp4" - path = Path(output_path) + + video_url, audio_url = _get_stream_urls(youtube_url) + if not video_url: + return + + if audio_url: + cmd = [ + "ffmpeg", + "-reconnect", "1", + "-reconnect_streamed", "1", + "-reconnect_delay_max", "5", + "-i", video_url, + "-reconnect", "1", + "-reconnect_streamed", "1", + "-reconnect_delay_max", "5", + "-i", audio_url, + "-c:v", "copy", + "-c:a", "aac", + "-movflags", "frag_keyframe+empty_moov+default_base_moof", + "-f", "mp4", + "pipe:1", + ] + else: + cmd = [ + "ffmpeg", + "-reconnect", "1", + "-reconnect_streamed", "1", + "-reconnect_delay_max", "5", + "-i", video_url, + "-c", "copy", + "-movflags", "frag_keyframe+empty_moov+default_base_moof", + "-f", "mp4", + "pipe:1", + ] process = subprocess.Popen( - [ - "yt-dlp", - "-f", "best[ext=mp4][vcodec^=avc]/best[ext=mp4]", - "-o", output_path, - youtube_url, - ], - stdout=subprocess.DEVNULL, + cmd, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, ) - # Warte bis Datei existiert und mindestens 1MB hat - while process.poll() is None: - if path.exists() and path.stat().st_size >= 1024 * 1024: - break - time.sleep(0.5) - - if not path.exists(): - process.wait() - return - - # Streame aus der wachsenden Datei - pos = 0 - stall_count = 0 - with open(output_path, "rb") as f: - while True: - chunk = f.read(1024 * 1024) - if chunk: - pos += len(chunk) - stall_count = 0 + try: + with open(output_path, "wb") as f: + while True: + chunk = process.stdout.read(CHUNK_SIZE) + if not chunk: + break + f.write(chunk) yield chunk - else: - if process.poll() is not None: - # Download fertig — restliche Bytes lesen - remaining = f.read() - if remaining: - yield remaining - break - stall_count += 1 - if stall_count > 60: # 30 Sekunden ohne neue Daten - break - time.sleep(0.5) + except GeneratorExit: + pass + finally: + if process.poll() is None: + process.kill() + process.wait() + if process.stdout: + process.stdout.close() + + path = Path(output_path) + if process.returncode != 0 and path.exists(): + path.unlink() diff --git a/backend/services/video_service.py b/backend/services/video_service.py index 4a78077..3734fb8 100644 --- a/backend/services/video_service.py +++ b/backend/services/video_service.py @@ -8,10 +8,11 @@ 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 profile_id: - profile = db.query(Profile).filter(Profile.id == profile_id).first() - if profile: - video.profiles.append(profile) + 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) diff --git a/browser_extension/background.js b/browser_extension/background.js index c35da08..0d1a737 100644 --- a/browser_extension/background.js +++ b/browser_extension/background.js @@ -1,4 +1,4 @@ -const SERVER_URL = "http://localhost:8000/videos"; +const SERVER_URL = "http://marha.local:8000/videos"; browser.runtime.onMessage.addListener((videos) => { fetch(SERVER_URL, {