commit 2dfa4e7563eb659b913e190010cadf0a65abb5df Author: Nicholas Ward Date: Fri Sep 18 15:19:27 2026 -0500 Prepare repository for public release Remove embedded credentials and private deployment configuration; document the remaining publication checks. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1d10bbe --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Generate a unique value, for example: openssl rand -hex 32 +API_TOKEN=replace-with-a-long-random-token + +# Directory containing the videos exposed by the server. +MEDIA_PATH=./sample-media diff --git a/.gitea/workflows/build-server-image.yml b/.gitea/workflows/build-server-image.yml new file mode 100644 index 0000000..d1fa46a --- /dev/null +++ b/.gitea/workflows/build-server-image.yml @@ -0,0 +1,36 @@ +name: Build server image + +on: + push: + branches: + - master + workflow_dispatch: + +jobs: + build: + runs-on: shared-ci + env: + IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY }} + IMAGE_NAME: ${{ vars.IMAGE_NAME }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to Gitea registry + uses: docker/login-action@v3 + with: + registry: ${{ env.IMAGE_REGISTRY }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Build image + run: docker build --tag "$IMAGE_REGISTRY/$IMAGE_NAME:${{ github.sha }}" --tag "$IMAGE_REGISTRY/$IMAGE_NAME:latest" server + + - name: Push image + run: | + docker push "$IMAGE_REGISTRY/$IMAGE_NAME:${{ github.sha }}" + docker push "$IMAGE_REGISTRY/$IMAGE_NAME:latest" + + - name: Prune images + if: always() + run: docker image prune --force diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99e088f --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.gradle/ +.idea/ +*.iml +local.properties +android/app/build/ +android/build/ +server/short-form-video-server +catalog-data/ +sample-media/* +!sample-media/.gitkeep +.env +.env.* +!.env.example +*.jks +*.keystore diff --git a/PUBLIC_RELEASE_CHECKLIST.md b/PUBLIC_RELEASE_CHECKLIST.md new file mode 100644 index 0000000..cfc84bd --- /dev/null +++ b/PUBLIC_RELEASE_CHECKLIST.md @@ -0,0 +1,21 @@ +# Public release checklist + +Complete these items while the repository is still private: + +- [ ] Choose a license for the original code and add it as `LICENSE`. +- [ ] Rotate the bearer token that was previously embedded in the application and + deployment configuration. +- [x] Rewrite Git history to remove the old bearer token and application PIN, + and replace the original commit author email with a noreply identity. +- [ ] Configure the Gitea Actions variables and secrets documented in `README.md`, + and verify the image build in a clean checkout. +- [ ] Review repository-adjacent data that is not in Git: issues, pull requests, + releases, packages, wiki pages, webhooks, deploy keys, collaborators, and + Actions logs/artifacts. +- [ ] Run a credential scanner against the rewritten history and inspect the final + tracked-file list before changing visibility. +- [ ] Enable appropriate branch protection and decide whether public issue and + pull-request creation should be enabled. + +Changing repository visibility is intentionally not part of this checklist's +automated preparation. diff --git a/README.md b/README.md new file mode 100644 index 0000000..54679e3 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# short-form-video + +A rapid prototype for browsing a private video library as a thumbnail grid and +playing its videos in a TikTok-style vertical pager. Content identity is +independent of filesystem layout, so videos can be moved or renamed safely. + +## Run the server + +The server reads its media directory through a read-only bind mount. Create a +local configuration file, generate a unique bearer token, and start it with: + +```sh +cp .env.example .env +sed -i "s/replace-with-a-long-random-token/$(openssl rand -hex 32)/" .env +docker compose up --build +``` + +Edit `MEDIA_PATH` in `.env` if your videos are not in `sample-media`. +The `.env` file is ignored by Git; never commit a real API token. + +The API is available at `http://localhost:8080`, requires bearer authentication, +and rescans every minute. + +You can also override either value for a single invocation: + +```sh +API_TOKEN="$(openssl rand -hex 32)" MEDIA_PATH="$PWD/sample-media" docker compose up --build +``` + +The SQLite catalog lives in the `catalog-data` Docker volume. Video files are +mounted read-only. SHA-256 content identity survives file moves and renames. +Identical files are represented as one logical video. Each scan synchronizes +read-only collections with the directories containing the videos. + +## Run the Android app + +Open the repository in Android Studio and run the `android:app` configuration, +or build from a shell with JDK 17 and an Android SDK: + +```sh +./gradlew :android:app:assembleDebug +``` + +Use `http://10.0.2.2:8080` from the Android emulator. On a physical device, use +the development computer's LAN address, such as `http://192.168.1.20:8080`. +Enter the same API token when connecting. The app stores its connection settings +in its private application data and excludes them from Android backups. Cleartext +HTTP is permitted only in debug builds; use HTTPS for non-local deployments. + +Tap a grid tile to open the vertical player. Long-press to select videos for a +manual collection. The pager prepares neighboring pages to reduce startup latency. +Swipe horizontally between the library and collections, or pull down to refresh. + +## Development checks + +```sh +cd server && go test ./... +docker compose config --quiet +./gradlew :android:app:assembleDebug +``` + +`ffmpeg` and `ffprobe` are included in the server image for thumbnails and media +metadata. The server otherwise uses Go's standard library plus one SQLite driver. + +## Container publishing + +The Gitea workflow expects these repository settings: + +- Variables: `IMAGE_REGISTRY` and `IMAGE_NAME` +- Secrets: `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` + +Forks do not inherit those secrets. Configure them before enabling the workflow +on a new repository. + +## Security + +Treat the bearer token as a password. The server provides a single shared token, +not per-user accounts or authorization scopes, and should not be exposed directly +to the internet without HTTPS and appropriate network controls. + +## Licensing + +Third-party licenses and attributions are recorded in +[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md). No license has yet been granted +for the repository's original code; choose and add one before announcing the +project as open source. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..ba1f717 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,43 @@ +# Third-party notices + +The Android interface draws visual and interaction inspiration from +[DaChelimo/TikTok-Clone](https://github.com/DaChelimo/TikTok-Clone), used under +the MIT License. + +Copyright (c) 2021 Andre-max + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Playback pooling, caching, lifecycle, and retry patterns are adapted +from [manjees/compose-reels](https://github.com/manjees/compose-reels), +Copyright 2024 manjees, licensed under the Apache License, Version 2.0. + +Additional Android Compose presentation patterns and icons are adapted from +[puskal-khadka/TikTok-Compose](https://github.com/puskal-khadka/TikTok-Compose), +Copyright 2023 Puskal Khadka, licensed under the Apache License, Version 2.0. + +The application bundles TikTok Sans from +[tiktok/TikTokSans](https://github.com/tiktok/TikTokSans), Copyright 2024 +TikTok Inc., licensed under the SIL Open Font License, Version 1.1. The full +font license is included in `android/app/src/main/res/raw/tiktok_sans_license.txt`. + +The collection checkbox sheet is adapted from +[cnrture/PickerSheet](https://github.com/cnrture/PickerSheet), licensed under +the Apache License, Version 2.0. Its picker layout, drag handle, title, lazy +list, checkbox row, spacing, and navigation-bar treatment are retained. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..6f3973d --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") +} + +android { + namespace = "com.uuard.shortformvideo" + compileSdk = 36 + + defaultConfig { + applicationId = "com.uuard.shortformvideo" + minSdk = 23 + targetSdk = 36 + versionCode = 1 + versionName = "0.1.0" + } + + buildTypes { + debug { manifestPlaceholders["usesCleartextTraffic"] = "true" } + release { + isMinifyEnabled = true + manifestPlaceholders["usesCleartextTraffic"] = "false" + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + } + } + buildFeatures { compose = true } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { jvmTarget = "17" } +} + +dependencies { + val media3 = "1.11.1" + implementation("androidx.activity:activity-compose:1.10.1") + implementation("androidx.compose.material3:material3:1.3.2") + implementation("androidx.compose.foundation:foundation:1.8.1") + implementation("androidx.compose.ui:ui:1.8.1") + implementation("androidx.compose.ui:ui-tooling-preview:1.8.1") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") + implementation("androidx.media3:media3-exoplayer:$media3") + implementation("androidx.media3:media3-database:$media3") + implementation("androidx.media3:media3-datasource:$media3") + implementation("androidx.media3:media3-ui:$media3") + implementation("io.coil-kt.coil3:coil-compose:3.2.0") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.2.0") + debugImplementation("androidx.compose.ui:ui-tooling:1.8.1") +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2bbc592 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/uuard/shortformvideo/ApiClient.kt b/android/app/src/main/java/com/uuard/shortformvideo/ApiClient.kt new file mode 100644 index 0000000..53bfd18 --- /dev/null +++ b/android/app/src/main/java/com/uuard/shortformvideo/ApiClient.kt @@ -0,0 +1,89 @@ +package com.uuard.shortformvideo + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.net.HttpURLConnection +import java.net.URI +import java.net.URLEncoder + +class ApiClient(serverUrl: String, private val apiToken: String) { + private val base = serverUrl.trimEnd('/') + "/" + + suspend fun videos(): List