본문으로 건너뛰기
AI NEWS JOURNAL
Hacker News Show HNAI NEWS / JOURNAL

Show HN: SeharaCloud – Self-Hosted Multimedia System

Article URL: https://github.com/SamerKolasevic29/SeharaCloud Comments URL: https://news.ycombinator.com/item?id=50494677 Points: 1 # Comments: 1

번역 서비스가 일시 중지되어 원문을 표시합니다.

Self-hosted media cloud for music, video, documents, and images. Files are automatically indexed (C++ daemon), metadata is stored in PostgreSQL, and a REST API (ASP.NET Core) serves them to clients (mobile app, web frontend...). This README covers the backend portion (indexer + API + database + deployment) and the Android mobile app. The frontend builds upon the API contract described here. Table of Contents Architecture Tech Stack Project Structure Getting Started Data Customization & Workflow Environment Variables (.env) appsettings.Production.json Indexer — Expectations and Inner Workings Database Mobile App — Overview Mobile App — Features Mobile App — Screenshots Mobile App — Tech Stack Mobile App — Project Structure Mobile App — Getting Started Mobile App — Configuring the API Base URL Mobile App — Build & Run Architecture flowchart LR A["scp / rsync / yt-dlp"] -->|write| B[("staging/")] B -->|"IN_CLOSE_WRITE / IN_MOVED_TO"| C["Watcher (inotify)"] C --> D["FileProcessor\nSHA-256 · TagLib · ffprobe · Poppler · Magick++"] D -->|INSERT metadata| E[("PostgreSQL")] D -->|move file| F[("media/ — physical")] G["ASP.NET Core API"] -->|SELECT| E G -->|"PhysicalFile + Range"| F H["Frontend / Mobile App"] -->|"REST"| G Typical file lifecycle: File arrives in staging/ (scp, rsync, or yt-dlp downloads mp3 directly there). Watcher captures IN_CLOSE_WRITE / IN_MOVED_TO (i.e., only after the write operation is complete — see the atomic write note below). FileProcessor calculates SHA-256, checks for duplicates (fn_find_duplicate), extracts metadata based on file type, and generates a .webp thumbnail if needed. File is moved from staging/ to the final physical directory; the logical path (/mnt/cloud/...) is written to the database, not the physical one. API reads from the database and serves files from its mount point (same host folder, different path inside the container — see below). Frontend calls REST endpoints; ThumbnailUrl in the response is already a full URL, ready for / . Tech Stack Project Structure /data/configs/seharacloud/api/ ├── client/SeharaCloud.MobileApp/ # Android mobile app ├── database/schema.sql # complete DDL, seed data, functions ├── docker-compose.yml # template (without real credentials) ├── docs/ApiContract.md # full API contract (source of truth for endpoints) ├── env.example # template for .env ├── server/ │ ├── api/SeharaCloud.api/ # ASP.NET Core API │ └── indexer/ # C++ inotify daemon The repository (this folder) contains template files (docker-compose.yml, env.example) without real credentials. The actual .env and appsettings.Production.json files with production values reside separately on the server at /data/docker/seharacloud/ and are never committed. Getting Started Prerequisites: Docker, Docker Compose, git. git clone https://github.com/SamerKolasevic29/SeharaCloud.git seharacloud cd seharacloud # 1. Environment & Config cp env.example .env cp appsettings.Production.json.example appsettings.Production.json # Edit .env and appsettings.Production.json with your actual credentials # 2. Build & Start Containers docker compose up -d --build # 3. Initialize Database (Crucial Step) docker exec -i seharacloud-db psql -U postgres -d seharacloud-db < database/schema.sql # 4. Setup Default Thumbnails # Ensure your host media folders (videos, images, music, documents, thumbnails, staging) exist. # Copy default thumbnails from the repo to your media/thumbnails/ folder: cp -r docs/DefaultThumbnails/* /path/to/your/media/thumbnails/ Data Customization & Workflow By default, every new song processed by the indexer will be assigned to an "Unknown Artist" and "Unknown Genre". If you want your media properly organized, follow these steps before dropping files into the staging folder. 1. Setting up Genres You can find pre-made thumbnails for common genres inside docs/GenreThumbnails. To add a genre to the database: Insert the genre (note that thumbnail_id1 is already the default fallback, so you only map thumbnail_id2): INSERT INTO genres (name, thumbnail_id2) VALUES ('Rock', 'your-thumbnail-uuid-here'); List your genres to get their IDs for later use: SELECT id, name FROM genres; 2. Setting up Artists & Thumbnails Find up to two images for your artist (e.g., Artist.jpg, Artist2.png), place them in a folder, and convert them to the required .webp format using this script: # Run this inside your image folder to convert jpg/jpeg/png/webp to standard webp for file in *.{jpg,jpeg,png,webp}; do if [ -f "$file" ]; then filename=$(basename "$file") name="${filename%.*}" ffmpeg -i "$file" -vf scale=512:512 "${name}Thumbnail.webp" fi done Move the generated ArtistThumbnail.webp and Artist2Thumbnail.webp to your media/thumbnails/ folder. Insert the artist into the database and link them to the previously created genre ID: INSERT INTO artists (name, genre_id, thumbnail_id2, thumbnail_id3) VALUES ('Smoke Mardeljano', 'genre-uuid-here', 'artist-thumb-uuid', 'artist2-thumb-uuid'); 3. Triggering the Indexer The indexer relies heavily on filename parsing. Rename your audio files exactly to this format: Artist - SongName.mp3 (or .wav). Drop them into the staging/ directory. The indexer will read the filename, match the artist in the database, and link the song. If the filename doesn't match this structure, it falls back to "Unknown Artist". Environment Variables (.env) Important: HOST_MEDIA_PATH is mounted in both containers, but at different internal paths — the indexer sees it as PHYSICAL_BASE_PATH (where it physically writes files), while the API sees it as LOGICAL_BASE_PATH (/mnt/cloud, where it serves PhysicalFile from). Therefore, the database stores the logical path, not the physical one — the API and indexer look at the same disk through two different "windows". appsettings.Production.json { "AppSettings": { "BaseUrl": "https://sehara.cloud.com" }, "ConnectionStrings": { "DefaultConnection": "Host=seharacloud-db;Port=5432;Database=seharacloud-db;Username=clouduser;Password=..." } } In production, docker-compose.yml overrides both keys via environment variables (AppSettings__BaseUrl, ConnectionStrings__DefaultConnection — double underscore is the ASP.NET Core convention for nested JSON keys). This file serves as a fallback for local runs without Docker. Indexer — Expectations and Inner Workings Watcher listens for IN_CLOSE_WRITE | IN_MOVED_TO on the staging directory — reacting only when the write operation completes, not when a file is created. Both scp (which writes directly to the final name) and rsync without --inplace (which writes to .tmp and renames) are safe for this pattern. For any other source filling the staging folder, the safest approach is write-to-temp + atomic rename on the same filesystem. Supported Formats Deduplication Each file is hashed (SHA-256) prior to insertion; fn_find_duplicate() checks the hash across all four tables (UNION ALL). If a duplicate is found, the file is deleted from staging and is not re-indexed. Database Full DDL is located in database/schema.sql — this is just an overview. Sentinel rows — Unknown Genre, Unknown Artist, and default thumbnails for each file type exist as fixed UUIDs (seed data), protected against deletion by a trigger (fn_protect_unknown_rows). Frontend takeaway: no genre/artist/thumbnail field is ever NULL — a fallback always exists. Useful functions: fn_resolve_artist(name) — exact match → fuzzy match (trigram) → fallback to "Unknown Artist" fn_find_duplicate(hash) — searches hash across all 4 tables View v_artists_full — artist + all three thumbnails + genre, joined Base URL comes from API_BASE_URL (e.g., https://sehara.cloud.com). Full contract (including upcoming modules) is in docs/ApiContract.md — below is what is currently implemented and verified (Music module). MusicDto: Id, Filename, ThumbnailUrl, SizeBytes, Title, Artist, Genre, DurationSec Short queries (< 2 characters) on search endpoints throw a validation error; /stream returns a not-found error if the ID does not exist in the database or if the file is physically missing from disk (File.Exists is checked prior to streaming). Exact HTTP status codes mapped by middleware — check Middleware/ (not covered in this README). Video/Document/Image endpoints are not yet confirmed in code — once completed, add them to this table following the same format. Mobile App — Overview client/SeharaCloud.MobileApp is a native Android client for SeharaCloud. It talks to the REST API described above and gives you a single place to browse and play your self-hosted music, videos, documents, and photos from your phone. The app is written entirely in Kotlin with Jetpack Compose and follows an MVVM-style structure: each screen has a ViewModel that exposes StateFlows loaded through Retrofit, and the Compose UI simply collects them. A single shared MusicPlayerViewModel is connected to a Media3 PlaybackService, so music keeps playing in the background and survives navigation between screens. The API address is not hardcoded — it is read from local.properties at build time and exposed to the code as BuildConfig.BASE_URL (see Configuring the API Base URL). Mobile App — Features Home screen with live search (min 2 characters), a "Recently added" section, and a sortable "All tracks" list (A–Z / Z–A, by title or by artist, using a Collator for the bs locale so č, ć, š, ž, đ are ordered correctly). Browse by Artists and Genres, each opening its own track list with the same sorting options. Mini and full-screen player with seek bar, previous/next, shuffle, and swipe on the cover art to change tracks. Tap the cover or the genre icon to jump to the artist or genre. Background playback with a media notification (Media3 MediaSession) and Range streaming. Home screen with live search, a "Recently added" section, and a grid of all videos. Player with three modes: floating, portrait, and landscape fullscreen, plus seek bar and previous/next. Documents Live search with "Recent" and "All" tabs. Built-in PDF viewer with vertical scrolling and double-tap zoom. Photo grid sorted by date (newest first), tap a photo to open it in the full-screen viewer. The app follows the API: modules whose endpoints are not yet finished on the backend (see the API section above) are only as complete as the server allows. Mobile App — Screenshots Mobile App — Tech Stack Mobile App — Project Structure client/SeharaCloud.MobileApp/ ├── build.gradle.kts # root Gradle config ├── local.properties.example # template for local.properties (BASE_URL) └── app/ ├── build.gradle.kts # module config, dependencies, BASE_URL -> BuildConfig ├── proguard-rules.pro └── src/ ├── test/ # unit tests ├── androidTest/ # instrumented tests └── main/ ├── AndroidManifest.xml ├── java/com/devfamily/sehara/ │ ├── MainActivity.kt # single activity, hosts Compose + navigation │ ├── data/ # API layer and models │ │ ├── RetroFitClient.kt # Retrofit instance, reads BuildConfig.BASE_URL │ │ ├── ApiService.kt # REST endpoint definitions │ │ ├── PlaybackService.kt # Media3 MediaSessionService (background playback) │ │ ├── SongItem.kt │ │ ├── ArtistItem.kt │ │ ├── GenreItem.kt │ │ ├── VideoItem.kt │ │ ├── DocumentItem.kt │ │ └── PhotoItem.kt │ └── ui/ │ ├── theme/ # Color.kt, Theme.kt, Type.kt │ ├── navigation/ # Routes.kt, AppNavigation.kt │ ├── components/ # reusable composables │ │ ├── AllTracksSection.kt │ │ ├── MusicListRow.kt │ │ ├── MusicPlayerOverlay.kt │ │ ├── VideoPlayerOverlay.kt │ │ ├── VideoListRow.kt │ │ ├── PhotoGridView.kt │ │ ├── PhotoDetailRow.kt │ │ ├── SearchBar.kt │ │ ├── MarqueeText.kt │ │ └── CategoryCard.kt │ └── screens/ │ ├── landing/ # LandingScreen │ ├── home/ # HomeScreen (module picker) │ ├── music/ # MusicHomeScreen, ArtistScreen, GenreScreen, FavoriteScreen │ │ # MusicListScreen, MusicFilter + ViewModels │ │ # (Music, Artist, Genre, MusicList, MusicPlayer) │ ├── video/ # VideoHomeScreen, VideoViewModel, VideoPlayerViewModel │ ├── docs/ # DocumentsScreen, DocumentViewerScreen, DocumentsViewModel │ └── photo/ # PhotoScreen, PhotoHostScreen, PhotoViewerScreen, PhotoViewModel └── res/ ├── drawable/ # icons (ic_*.xml), card and splash backgrounds ├── font/ # Lexend (regular, medium, semibold, bold) ├── mipmap-*/ # launcher icons ├── values/ values-v29/ # colors, strings, themes └── xml/ # network_security_config, backup and extraction rules Mobile App — Getting Started Prerequisites: A running SeharaCloud backend reachable from your phone/emulator (see Getting Started above). Android Studio (recent stable version) with Android SDK 36 installed. An Android device or emulator running Android 8.0 (API 26) or newer. git clone https://github.com/SamerKolasevic29/SeharaCloud.git seharacloud cd seharacloud/client/SeharaCloud.MobileApp Mobile App — Configuring the API Base URL The app does not ship with a server address. You provide yours in local.properties, which is read by app/build.gradle.kts and injected into BuildConfig.BASE_URL. This file is local to your machine and is never committed. # 1. Create your local.properties from the template cp local.properties.example local.properties # 2. Edit it and set your API address Content of local.properties: # BASE_URL must end with "/" BASE_URL=https://example.com/ Replace https://example.com/ with the same public URL you set as API_BASE_URL in the backend .env (e.g., https://sehara.cloud.com/). Important: BASE_URL must end with a trailing slash (/) — Retrofit refuses to start without it. The value is baked into the app at build time. After changing it, rebuild the app (Android Studio: Build → Rebuild Project). If local.properties already exists (Android Studio creates it automatically with the sdk.dir line), just add the BASE_URL line to it instead of overwriting the file. For a plain http:// server (e.g., local testing in your home network), allow that host in app/src/main/res/xml/network_security_config.xml, otherwise Android blocks cleartext traffic. From an emulator, the host machine is reachable at 10.0.2.2. Mobile App — Build & Run Android Studio (recommended) Open the client/SeharaCloud.MobileApp folder in Android Studio. Wait for the Gradle sync to finish. Select a device or emulator (API 26+). Press Run (Shift + F10). Command line cd client/SeharaCloud.MobileApp # Build a debug APK ./gradlew assembleDebug # Install on a connected device / running emulator ./gradlew installDebug The debug APK is written to app/build/outputs/apk/debug/app-debug.apk.

자료 출처

원문 읽기 ↗

관련 주제

← 전체 소식 이어 읽기