update
This commit is contained in:
@@ -6,5 +6,11 @@ data class Video(
|
|||||||
val youtuber: String,
|
val youtuber: String,
|
||||||
val thumbnail_url: String,
|
val thumbnail_url: String,
|
||||||
val youtube_url: String,
|
val youtube_url: String,
|
||||||
val is_downloaded: Boolean
|
val is_downloaded: Boolean,
|
||||||
|
val profile_ids: List<Int> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Profile(
|
||||||
|
val id: Int,
|
||||||
|
val name: String
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,14 +3,18 @@ package com.youtubeapp.data
|
|||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface VideoApi {
|
interface VideoApi {
|
||||||
@GET("videos")
|
@GET("videos")
|
||||||
suspend fun getAllVideos(): List<Video>
|
suspend fun getAllVideos(@Query("profile_id") profileId: Int? = null): List<Video>
|
||||||
|
|
||||||
@GET("videos/downloaded")
|
@GET("videos/downloaded")
|
||||||
suspend fun getDownloadedVideos(): List<Video>
|
suspend fun getDownloadedVideos(@Query("profile_id") profileId: Int? = null): List<Video>
|
||||||
|
|
||||||
@POST("videos/{id}/download")
|
@POST("videos/{id}/download")
|
||||||
suspend fun triggerDownload(@Path("id") id: Int): Map<String, String>
|
suspend fun triggerDownload(@Path("id") id: Int): Map<String, String>
|
||||||
|
|
||||||
|
@GET("profiles")
|
||||||
|
suspend fun getProfiles(): List<Profile>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ package com.youtubeapp.data
|
|||||||
|
|
||||||
class VideoRepository(private val api: VideoApi = ApiClient.api) {
|
class VideoRepository(private val api: VideoApi = ApiClient.api) {
|
||||||
|
|
||||||
suspend fun getAllVideos(): Result<List<Video>> = runCatching { api.getAllVideos() }
|
suspend fun getAllVideos(profileId: Int? = null): Result<List<Video>> =
|
||||||
|
runCatching { api.getAllVideos(profileId) }
|
||||||
|
|
||||||
suspend fun getDownloadedVideos(): Result<List<Video>> = runCatching { api.getDownloadedVideos() }
|
suspend fun getDownloadedVideos(profileId: Int? = null): Result<List<Video>> =
|
||||||
|
runCatching { api.getDownloadedVideos(profileId) }
|
||||||
|
|
||||||
suspend fun triggerDownload(videoId: Int): Result<String> = runCatching {
|
suspend fun triggerDownload(videoId: Int): Result<String> = runCatching {
|
||||||
val response = api.triggerDownload(videoId)
|
val response = api.triggerDownload(videoId)
|
||||||
response["status"] ?: "unknown"
|
response["status"] ?: "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getProfiles(): Result<List<Profile>> = runCatching { api.getProfiles() }
|
||||||
|
|
||||||
fun getStreamUrl(videoId: Int): String = "${ApiClient.BASE_URL}videos/$videoId/stream"
|
fun getStreamUrl(videoId: Int): String = "${ApiClient.BASE_URL}videos/$videoId/stream"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,25 @@ package com.youtubeapp.ui.navigation
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
import androidx.compose.material.icons.filled.VideoLibrary
|
import androidx.compose.material.icons.filled.VideoLibrary
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -27,20 +38,60 @@ import com.youtubeapp.ui.screens.VideoDetailScreen
|
|||||||
import com.youtubeapp.ui.screens.VideoPlayerScreen
|
import com.youtubeapp.ui.screens.VideoPlayerScreen
|
||||||
import com.youtubeapp.ui.viewmodel.VideoViewModel
|
import com.youtubeapp.ui.viewmodel.VideoViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavigation() {
|
fun AppNavigation() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val viewModel: VideoViewModel = viewModel()
|
val viewModel: VideoViewModel = viewModel()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
viewModel.init(context)
|
viewModel.init(context)
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentRoute = navBackStackEntry?.destination?.route
|
val currentRoute = navBackStackEntry?.destination?.route
|
||||||
|
|
||||||
val showBottomBar = currentRoute in listOf(Route.AllVideos.route, Route.Downloaded.route)
|
val showBars = currentRoute in listOf(Route.AllVideos.route, Route.Downloaded.route)
|
||||||
|
var showProfileMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.loadProfiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectedProfileName = state.profiles.find { it.id == state.selectedProfileId }?.name ?: "Profil"
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
if (showBars) {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("YouTube App") },
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { showProfileMenu = true }) {
|
||||||
|
Icon(Icons.Default.Person, contentDescription = "Profil")
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showProfileMenu,
|
||||||
|
onDismissRequest = { showProfileMenu = false }
|
||||||
|
) {
|
||||||
|
for (profile in state.profiles) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
if (profile.id == state.selectedProfileId) "✓ ${profile.name}"
|
||||||
|
else profile.name
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
viewModel.selectProfile(profile.id)
|
||||||
|
showProfileMenu = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (showBottomBar) {
|
if (showBars) {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = currentRoute == Route.AllVideos.route,
|
selected = currentRoute == Route.AllVideos.route,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.youtubeapp.data.LocalStorageService
|
import com.youtubeapp.data.LocalStorageService
|
||||||
|
import com.youtubeapp.data.Profile
|
||||||
import com.youtubeapp.data.Video
|
import com.youtubeapp.data.Video
|
||||||
import com.youtubeapp.data.VideoRepository
|
import com.youtubeapp.data.VideoRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -15,6 +16,8 @@ import kotlinx.coroutines.launch
|
|||||||
data class VideoUiState(
|
data class VideoUiState(
|
||||||
val allVideos: List<Video> = emptyList(),
|
val allVideos: List<Video> = emptyList(),
|
||||||
val downloadedVideos: List<Video> = emptyList(),
|
val downloadedVideos: List<Video> = emptyList(),
|
||||||
|
val profiles: List<Profile> = emptyList(),
|
||||||
|
val selectedProfileId: Int? = null,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val isDownloading: Boolean = false,
|
val isDownloading: Boolean = false,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
@@ -24,6 +27,7 @@ data class VideoUiState(
|
|||||||
class VideoViewModel : ViewModel() {
|
class VideoViewModel : ViewModel() {
|
||||||
private val repository = VideoRepository()
|
private val repository = VideoRepository()
|
||||||
private var localStorage: LocalStorageService? = null
|
private var localStorage: LocalStorageService? = null
|
||||||
|
private var prefs: android.content.SharedPreferences? = null
|
||||||
private val _state = MutableStateFlow(VideoUiState())
|
private val _state = MutableStateFlow(VideoUiState())
|
||||||
val state: StateFlow<VideoUiState> = _state
|
val state: StateFlow<VideoUiState> = _state
|
||||||
|
|
||||||
@@ -31,12 +35,38 @@ class VideoViewModel : ViewModel() {
|
|||||||
if (localStorage == null) {
|
if (localStorage == null) {
|
||||||
localStorage = LocalStorageService(context.applicationContext)
|
localStorage = LocalStorageService(context.applicationContext)
|
||||||
}
|
}
|
||||||
|
if (prefs == null) {
|
||||||
|
prefs = context.applicationContext.getSharedPreferences("youtubeapp", Context.MODE_PRIVATE)
|
||||||
|
val savedId = prefs?.getInt("profile_id", -1) ?: -1
|
||||||
|
if (savedId > 0) {
|
||||||
|
_state.value = _state.value.copy(selectedProfileId = savedId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadProfiles() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
repository.getProfiles()
|
||||||
|
.onSuccess { profiles ->
|
||||||
|
_state.value = _state.value.copy(profiles = profiles)
|
||||||
|
if (_state.value.selectedProfileId == null && profiles.isNotEmpty()) {
|
||||||
|
selectProfile(profiles.first().id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectProfile(profileId: Int) {
|
||||||
|
_state.value = _state.value.copy(selectedProfileId = profileId)
|
||||||
|
prefs?.edit()?.putInt("profile_id", profileId)?.apply()
|
||||||
|
loadAllVideos()
|
||||||
|
loadDownloadedVideos()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAllVideos() {
|
fun loadAllVideos() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.value = _state.value.copy(isLoading = true, error = null)
|
_state.value = _state.value.copy(isLoading = true, error = null)
|
||||||
repository.getAllVideos()
|
repository.getAllVideos(profileId = _state.value.selectedProfileId)
|
||||||
.onSuccess { videos ->
|
.onSuccess { videos ->
|
||||||
_state.value = _state.value.copy(allVideos = videos, isLoading = false)
|
_state.value = _state.value.copy(allVideos = videos, isLoading = false)
|
||||||
}
|
}
|
||||||
@@ -49,7 +79,7 @@ class VideoViewModel : ViewModel() {
|
|||||||
fun loadDownloadedVideos() {
|
fun loadDownloadedVideos() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.value = _state.value.copy(isLoading = true, error = null)
|
_state.value = _state.value.copy(isLoading = true, error = null)
|
||||||
repository.getDownloadedVideos()
|
repository.getDownloadedVideos(profileId = _state.value.selectedProfileId)
|
||||||
.onSuccess { videos ->
|
.onSuccess { videos ->
|
||||||
_state.value = _state.value.copy(downloadedVideos = videos, isLoading = false)
|
_state.value = _state.value.copy(downloadedVideos = videos, isLoading = false)
|
||||||
}
|
}
|
||||||
@@ -63,7 +93,6 @@ class VideoViewModel : ViewModel() {
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
_state.value = _state.value.copy(isDownloading = true, downloadStatus = null)
|
_state.value = _state.value.copy(isDownloading = true, downloadStatus = null)
|
||||||
try {
|
try {
|
||||||
// 1. Server-Download triggern
|
|
||||||
val result = repository.triggerDownload(videoId)
|
val result = repository.triggerDownload(videoId)
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
@@ -73,7 +102,6 @@ class VideoViewModel : ViewModel() {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Warten bis Server-Download fertig
|
|
||||||
val status = result.getOrNull()
|
val status = result.getOrNull()
|
||||||
if (status == "download_started") {
|
if (status == "download_started") {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -84,7 +112,6 @@ class VideoViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Lokal speichern
|
|
||||||
localStorage?.downloadAndSave(videoId)
|
localStorage?.downloadAndSave(videoId)
|
||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
isDownloading = false,
|
isDownloading = false,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from database import create_tables
|
from database import SessionLocal, create_tables
|
||||||
from routes.videos import router as videos_router
|
from models import Profile
|
||||||
|
from routes.videos import profiles_router, router as videos_router
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
@@ -14,11 +15,17 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
app.include_router(videos_router)
|
app.include_router(videos_router)
|
||||||
|
app.include_router(profiles_router)
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
def startup():
|
def startup():
|
||||||
create_tables()
|
create_tables()
|
||||||
|
db = SessionLocal()
|
||||||
|
if db.query(Profile).count() == 0:
|
||||||
|
db.add(Profile(name="Standard"))
|
||||||
|
db.commit()
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
from sqlalchemy import Column, Integer, String
|
from sqlalchemy import Column, ForeignKey, Integer, String, Table
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from database import Base
|
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):
|
class Video(Base):
|
||||||
__tablename__ = "videos"
|
__tablename__ = "videos"
|
||||||
@@ -12,3 +27,4 @@ class Video(Base):
|
|||||||
thumbnail_url = Column(String, nullable=False)
|
thumbnail_url = Column(String, nullable=False)
|
||||||
youtube_url = Column(String, nullable=False)
|
youtube_url = Column(String, nullable=False)
|
||||||
file_path = Column(String, nullable=True)
|
file_path = Column(String, nullable=True)
|
||||||
|
profiles = relationship("Profile", secondary=video_profiles, backref="videos")
|
||||||
|
|||||||
Binary file not shown.
@@ -1,12 +1,13 @@
|
|||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from database import get_db
|
from database import get_db
|
||||||
from schemas import VideoCreate, VideoResponse
|
from schemas import ProfileResponse, VideoCreate, VideoResponse
|
||||||
from services import video_service
|
from services import video_service
|
||||||
from services.download_service import download_video
|
from services.download_service import download_video
|
||||||
from services.stream_service import stream_video_live
|
from services.stream_service import stream_video_live
|
||||||
@@ -27,14 +28,14 @@ def create_videos(videos_data: list[VideoCreate], db: Session = Depends(get_db))
|
|||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[VideoResponse])
|
@router.get("", response_model=list[VideoResponse])
|
||||||
def get_all_videos(db: Session = Depends(get_db)):
|
def get_all_videos(profile_id: Optional[int] = Query(None), db: Session = Depends(get_db)):
|
||||||
videos = video_service.get_all_videos(db)
|
videos = video_service.get_all_videos(db, profile_id=profile_id)
|
||||||
return [VideoResponse.from_model(v) for v in videos]
|
return [VideoResponse.from_model(v) for v in videos]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/downloaded", response_model=list[VideoResponse])
|
@router.get("/downloaded", response_model=list[VideoResponse])
|
||||||
def get_downloaded_videos(db: Session = Depends(get_db)):
|
def get_downloaded_videos(profile_id: Optional[int] = Query(None), db: Session = Depends(get_db)):
|
||||||
videos = video_service.get_downloaded_videos(db)
|
videos = video_service.get_downloaded_videos(db, profile_id=profile_id)
|
||||||
return [VideoResponse.from_model(v) for v in videos]
|
return [VideoResponse.from_model(v) for v in videos]
|
||||||
|
|
||||||
|
|
||||||
@@ -88,3 +89,11 @@ def download_file(video_id: int, db: Session = Depends(get_db)):
|
|||||||
raise HTTPException(status_code=404, detail="Videodatei nicht gefunden")
|
raise HTTPException(status_code=404, detail="Videodatei nicht gefunden")
|
||||||
|
|
||||||
return FileResponse(path, media_type="video/mp4", filename=f"{video.title}.mp4")
|
return FileResponse(path, media_type="video/mp4", filename=f"{video.title}.mp4")
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class VideoCreate(BaseModel):
|
|||||||
youtuber: str
|
youtuber: str
|
||||||
thumbnail_url: str
|
thumbnail_url: str
|
||||||
youtube_url: str
|
youtube_url: str
|
||||||
|
profile_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class VideoResponse(BaseModel):
|
class VideoResponse(BaseModel):
|
||||||
@@ -15,6 +16,7 @@ class VideoResponse(BaseModel):
|
|||||||
thumbnail_url: str
|
thumbnail_url: str
|
||||||
youtube_url: str
|
youtube_url: str
|
||||||
is_downloaded: bool
|
is_downloaded: bool
|
||||||
|
profile_ids: list[int]
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@@ -28,4 +30,13 @@ class VideoResponse(BaseModel):
|
|||||||
thumbnail_url=video.thumbnail_url,
|
thumbnail_url=video.thumbnail_url,
|
||||||
youtube_url=video.youtube_url,
|
youtube_url=video.youtube_url,
|
||||||
is_downloaded=video.file_path is not None,
|
is_downloaded=video.file_path is not None,
|
||||||
|
profile_ids=[p.id for p in video.profiles],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|||||||
Binary file not shown.
@@ -1,23 +1,35 @@
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from models import Video
|
from models import Profile, Video, video_profiles
|
||||||
from schemas import VideoCreate
|
from schemas import VideoCreate
|
||||||
|
|
||||||
|
|
||||||
def create_video(db: Session, video_data: VideoCreate) -> Video:
|
def create_video(db: Session, video_data: VideoCreate) -> Video:
|
||||||
video = Video(**video_data.model_dump())
|
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)
|
||||||
db.add(video)
|
db.add(video)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(video)
|
db.refresh(video)
|
||||||
return video
|
return video
|
||||||
|
|
||||||
|
|
||||||
def get_all_videos(db: Session) -> list[Video]:
|
def get_all_videos(db: Session, profile_id: int | None = None) -> list[Video]:
|
||||||
return db.query(Video).order_by(Video.id.desc()).all()
|
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) -> list[Video]:
|
def get_downloaded_videos(db: Session, profile_id: int | None = None) -> list[Video]:
|
||||||
return db.query(Video).filter(Video.file_path.isnot(None)).order_by(Video.id.desc()).all()
|
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:
|
def get_video(db: Session, video_id: int) -> Video | None:
|
||||||
@@ -34,3 +46,7 @@ def update_file_path(db: Session, video_id: int, path: str):
|
|||||||
if video:
|
if video:
|
||||||
video.file_path = path
|
video.file_path = path
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_profiles(db: Session) -> list[Profile]:
|
||||||
|
return db.query(Profile).all()
|
||||||
|
|||||||
@@ -40,13 +40,16 @@ function collectVideos(elements) {
|
|||||||
let pendingVideos = [];
|
let pendingVideos = [];
|
||||||
let sendTimer = null;
|
let sendTimer = null;
|
||||||
|
|
||||||
function queueVideos(videos) {
|
async function queueVideos(videos) {
|
||||||
pendingVideos.push(...videos);
|
pendingVideos.push(...videos);
|
||||||
if (!sendTimer) {
|
if (!sendTimer) {
|
||||||
sendTimer = setTimeout(() => {
|
sendTimer = setTimeout(async () => {
|
||||||
if (pendingVideos.length > 0) {
|
if (pendingVideos.length > 0) {
|
||||||
console.log(`[YT-Erfasser] ${pendingVideos.length} Videos senden`);
|
const stored = await browser.storage.local.get("profileId");
|
||||||
browser.runtime.sendMessage(pendingVideos);
|
const profileId = stored.profileId || null;
|
||||||
|
const batch = pendingVideos.map((v) => ({ ...v, profile_id: profileId }));
|
||||||
|
console.log(`[YT-Erfasser] ${batch.length} Videos senden (Profil: ${profileId})`);
|
||||||
|
browser.runtime.sendMessage(batch);
|
||||||
}
|
}
|
||||||
pendingVideos = [];
|
pendingVideos = [];
|
||||||
sendTimer = null;
|
sendTimer = null;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"description": "Erfasst YouTube-Videos und sendet sie an den Server",
|
"description": "Erfasst YouTube-Videos und sendet sie an den Server",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"*://www.youtube.com/*",
|
"*://www.youtube.com/*",
|
||||||
"http://localhost:8000/*"
|
"http://localhost:8000/*",
|
||||||
|
"storage"
|
||||||
],
|
],
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
@@ -15,5 +16,9 @@
|
|||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["background.js"]
|
"scripts": ["background.js"]
|
||||||
|
},
|
||||||
|
"browser_action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_title": "Profil auswählen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
browser_extension/popup.html
Normal file
16
browser_extension/popup.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { width: 200px; padding: 10px; font-family: sans-serif; font-size: 14px; }
|
||||||
|
h3 { margin: 0 0 8px; }
|
||||||
|
label { display: block; padding: 4px 0; cursor: pointer; }
|
||||||
|
.error { color: red; font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3>Profil</h3>
|
||||||
|
<div id="profiles"></div>
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
30
browser_extension/popup.js
Normal file
30
browser_extension/popup.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const SERVER_URL = "http://localhost:8000/profiles";
|
||||||
|
const container = document.getElementById("profiles");
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(SERVER_URL);
|
||||||
|
const profiles = await res.json();
|
||||||
|
const stored = await browser.storage.local.get("profileId");
|
||||||
|
const selectedId = stored.profileId || null;
|
||||||
|
|
||||||
|
for (const profile of profiles) {
|
||||||
|
const label = document.createElement("label");
|
||||||
|
const radio = document.createElement("input");
|
||||||
|
radio.type = "radio";
|
||||||
|
radio.name = "profile";
|
||||||
|
radio.value = profile.id;
|
||||||
|
radio.checked = profile.id === selectedId;
|
||||||
|
radio.addEventListener("change", () => {
|
||||||
|
browser.storage.local.set({ profileId: profile.id });
|
||||||
|
});
|
||||||
|
label.appendChild(radio);
|
||||||
|
label.appendChild(document.createTextNode(" " + profile.name));
|
||||||
|
container.appendChild(label);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
container.innerHTML = '<span class="error">Server nicht erreichbar</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
Reference in New Issue
Block a user