7 Commits
Author SHA1 Message Date
chrisandClaude Opus 5.5 3a462ee146 Remove the seriouscritter notifications section
Reverts 6537c34 and its syntax fix 4bdfae1. The section only regrouped a
user's own notifications from that account (duplicating them in the list),
and the actorId filter it added to listNotifications was never used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQFD3mtUWSZpeLbKzkCohh
2026-10-26 11:42:54 -04:00
chrisandClaude Opus 5.5 4bdfae155a Fix JSX syntax error on notifications page
The body span was missing its closing brace, which broke typecheck and
the web Docker build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:29:54 -04:00
chrisandClaude Opus 5.5 5778035656 Version 1.5.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:29:54 -04:00
chrisandClaude Opus 5.5 0a28f05d53 Don't share phone videos and screen recordings as now-playing
General-purpose players (VLC, MX Player, Photos, galleries) report a local
file's name as the title. Skip sessions whose title looks like a video file
or camera/recorder name, or that come from such a player without a real
artist tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:29:54 -04:00
chrisandaider 6537c34a38 feat: Display dedicated notifications section for seriouscritter
Co-authored-by: aider (ollama_chat/hf.co/TrevorJS/gemma-4-E4B-it-uncensored-GGUF:Q4_K_M) <aider@aider.chat>
2026-10-26 11:29:54 -04:00
chrisandClaude Opus 5.5 094f116b10 CI: test builds' publish step works under plain sh
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
2026-10-22 12:01:20 -04:00
chrisandClaude Opus 5.5 c7bd8f0ad3 Faster Android builds: ccache, Gradle build cache, 64-bit only, no release lint
Most of a build was compiling the same C++ twice per APK with nothing reused between builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
2026-10-15 15:21:35 -04:00
12 changed files with 87 additions and 18 deletions
+3 -3
View File
@@ -22,8 +22,8 @@ jobs:
runs-on: musicshare-android runs-on: musicshare-android
container: container:
image: musicshare-android-builder image: musicshare-android-builder
# Keep Gradle and npm downloads between builds. # Keep Gradle (downloads and build cache), npm, and compiled C++ (ccache) between builds.
options: -v musicshare-gradle:/root/.gradle -v musicshare-npm:/root/.npm options: -v musicshare-gradle:/root/.gradle -v musicshare-npm:/root/.npm -v musicshare-ccache:/root/.ccache
timeout-minutes: 150 timeout-minutes: 150
env: env:
SERVER: ${{ github.server_url }} SERVER: ${{ github.server_url }}
@@ -117,7 +117,7 @@ jobs:
echo "Release: $SERVER/$REPO/releases/tag/$REF_NAME" echo "Release: $SERVER/$REPO/releases/tag/$REF_NAME"
else else
owner=${REPO%%/*} owner=${REPO%%/*}
version="$(echo "$REF_NAME" | tr -c 'A-Za-z0-9._-\n' '-')-${SHA::8}" version="$(printf '%s' "$REF_NAME" | tr -c 'A-Za-z0-9._-' '-')-$(printf '%s' "$SHA" | cut -c1-8)"
for f in *.apk SHA256SUMS; do for f in *.apk SHA256SUMS; do
curl -fsS -X PUT -H "$auth" --upload-file "$f" "$SERVER/api/packages/$owner/generic/musicshare-android/$version/$f" curl -fsS -X PUT -H "$auth" --upload-file "$f" "$SERVER/api/packages/$owner/generic/musicshare-android/$version/$f"
echo "Uploaded $f" echo "Uploaded $f"
+2
View File
@@ -10,3 +10,5 @@ next-env.d.ts
# Production secrets and database backups # Production secrets and database backups
deploy/production.env deploy/production.env
backups/ backups/
.aider*
deploy/gitea-runner/data/
+2 -2
View File
@@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "musicshare", "name": "musicshare",
"slug": "musicshare", "slug": "musicshare",
"version": "1.4.0", "version": "1.5.0",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "musicshare", "scheme": "musicshare",
@@ -23,7 +23,7 @@
}, },
"predictiveBackGestureEnabled": false, "predictiveBackGestureEnabled": false,
"package": "tech.chrisedwards.musicshare", "package": "tech.chrisedwards.musicshare",
"versionCode": 5 "versionCode": 6
}, },
"plugins": [ "plugins": [
"expo-router", "expo-router",
@@ -121,9 +121,11 @@ class MediaListenerService : NotificationListenerService() {
private fun snapshot(controller: MediaController): Playback? { private fun snapshot(controller: MediaController): Playback? {
val metadata = controller.metadata val metadata = controller.metadata
val title = metadata?.text(MediaMetadata.METADATA_KEY_TITLE) ?: metadata?.text(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) val title = metadata?.text(MediaMetadata.METADATA_KEY_TITLE) ?: metadata?.text(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
val artist = metadata?.text(MediaMetadata.METADATA_KEY_ARTIST) val realArtist = metadata?.text(MediaMetadata.METADATA_KEY_ARTIST)
?: metadata?.text(MediaMetadata.METADATA_KEY_ALBUM_ARTIST) ?: metadata?.text(MediaMetadata.METADATA_KEY_ALBUM_ARTIST)
?: metadata?.text(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE) val artist = realArtist ?: metadata?.text(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE)
// A phone video or other non-song: treat the session as if it weren't there.
if (NonMusicFilter.isNonMusic(controller.packageName, title, realArtist)) return null
val playbackState = controller.playbackState val playbackState = controller.playbackState
val state = when (playbackState?.state) { val state = when (playbackState?.state) {
PlaybackState.STATE_PLAYING, PlaybackState.STATE_BUFFERING, PlaybackState.STATE_PLAYING, PlaybackState.STATE_BUFFERING,
@@ -0,0 +1,46 @@
package tech.chrisedwards.musicshare.nowplaying
/**
* Keeps things that aren't songs (phone videos, screen recordings) out of what we share.
* General-purpose players report a local file's name as its title, so a video played
* in VLC would otherwise show up as "VID_20261025_181502.mp4".
*/
internal object NonMusicFilter {
/** Players that also open videos and arbitrary files: only share from them when there's a real artist. */
private val GENERAL_PLAYERS = setOf(
"org.videolan.vlc",
"org.videolan.vlc.debug",
"com.mxtech.videoplayer.ad",
"com.mxtech.videoplayer.pro",
"is.xyz.mpv",
"com.brouken.player", // Just Player
"com.google.android.apps.photos",
"com.sec.android.gallery3d",
"com.android.gallery3d",
"com.miui.gallery",
"com.google.android.apps.nbu.files",
"com.sec.android.app.myfiles",
)
private val FILE_NAME = Regex(
""".*\.(mp4|m4v|mkv|mov|webm|avi|3gp|3g2|wmv|flv|ts|mts|m2ts|mpg|mpeg)$""",
RegexOption.IGNORE_CASE,
)
/** Camera, screen-recorder and messaging-app naming schemes. */
private val RECORDING_NAME = Regex(
"""^(VID|PXL|IMG|MVIMG|MOV|DSC|GOPR|GX\d{2}|Screen[_ ]?Recording|Screenrecorder|Record|signal|WhatsApp Video)[-_ ]?\d.*""",
RegexOption.IGNORE_CASE,
)
/**
* [realArtist] is the artist/album-artist tag only, not the display-subtitle fallback,
* which players fill with folder names and the like.
*/
fun isNonMusic(packageName: String, title: String?, realArtist: String?): Boolean {
val name = title?.trim().orEmpty()
if (name.isEmpty()) return false
if (FILE_NAME.matches(name) || RECORDING_NAME.matches(name)) return true
return packageName in GENERAL_PLAYERS && realArtist.isNullOrBlank()
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@musicshare/mobile", "name": "@musicshare/mobile",
"main": "expo-router/entry", "main": "expo-router/entry",
"version": "1.4.0", "version": "1.5.0",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@expo-google-fonts/poppins": "^0.4.1", "@expo-google-fonts/poppins": "^0.4.1",
+18 -4
View File
@@ -7,24 +7,37 @@
# #
# Both are signed with the upload key when it's configured (see plugins/withReleaseSigning.js; # Both are signed with the upload key when it's configured (see plugins/withReleaseSigning.js;
# in CI it comes from ORG_GRADLE_PROJECT_MUSICSHARE_UPLOAD_* variables). Set REQUIRE_SIGNED=1 to # in CI it comes from ORG_GRADLE_PROJECT_MUSICSHARE_UPLOAD_* variables). Set REQUIRE_SIGNED=1 to
# fail rather than produce unsigned APKs. ABIS picks the CPU types (default: phones, 32 and 64 bit). # fail rather than produce unsigned APKs. ABIS picks the CPU types (default: 64-bit phones, which is
# Regenerates apps/mobile/android each time (prebuild --clean). # nearly every phone from the last several years; ABIS=armeabi-v7a,arm64-v8a adds old 32-bit ones).
# Regenerates apps/mobile/android each time (prebuild --clean); Gradle's build cache and ccache (when
# installed) make the rebuild cheap anyway.
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
OUT=$(realpath -m "${1:-dist}") OUT=$(realpath -m "${1:-dist}")
VERSION=$(node -p "require('./app.json').expo.version") VERSION=$(node -p "require('./app.json').expo.version")
ABIS=${ABIS:-armeabi-v7a,arm64-v8a} ABIS=${ABIS:-arm64-v8a}
BUILD_TOOLS=$(ls -d "$ANDROID_HOME"/build-tools/*/ | sort -V | tail -1) BUILD_TOOLS=$(ls -d "$ANDROID_HOME"/build-tools/*/ | sort -V | tail -1)
mkdir -p "$OUT" mkdir -p "$OUT"
say() { printf '\n==> %s\n' "$*"; } say() { printf '\n==> %s\n' "$*"; }
# Most of a build is compiling the C++ in React Native and its libraries, which rarely changes:
# with ccache, CMake (3.17+) reuses earlier results via these launcher variables.
if command -v ccache > /dev/null; then
export CMAKE_C_COMPILER_LAUNCHER=ccache CMAKE_CXX_COMPILER_LAUNCHER=ccache
export CCACHE_BASEDIR=${CCACHE_BASEDIR:-$PWD} CCACHE_COMPILERCHECK=content CCACHE_MAXSIZE=${CCACHE_MAXSIZE:-6G}
ccache --zero-stats > /dev/null
fi
build() { build() {
local suffix=$1 foss=$2 local suffix=$1 foss=$2
say "Building musicshare-$VERSION$suffix.apk" say "Building musicshare-$VERSION$suffix.apk"
MUSICSHARE_FOSS=$foss CI=1 npx expo prebuild --platform android --clean --no-install MUSICSHARE_FOSS=$foss CI=1 npx expo prebuild --platform android --clean --no-install
(cd android && ./gradlew :app:assembleRelease -PreactNativeArchitectures="$ABIS" --console=plain) # --build-cache reuses compiled Kotlin/Java, dex and resources from earlier builds (kept in ~/.gradle).
# Release lint is skipped: it takes a while and TypeScript, ESLint and the tests cover what matters.
(cd android && ./gradlew :app:assembleRelease -PreactNativeArchitectures="$ABIS" --build-cache \
-x lintVitalAnalyzeRelease -x lintVitalReportRelease -x lintVitalRelease --console=plain)
local apk local apk
apk=$(ls android/app/build/outputs/apk/release/*.apk | head -1) apk=$(ls android/app/build/outputs/apk/release/*.apk | head -1)
@@ -56,3 +69,4 @@ build() {
build "" 0 build "" 0
build "-foss" 1 build "-foss" 1
(cd "$OUT" && sha256sum musicshare-"$VERSION"*.apk > SHA256SUMS && cat SHA256SUMS) (cd "$OUT" && sha256sum musicshare-"$VERSION"*.apk > SHA256SUMS && cat SHA256SUMS)
if command -v ccache > /dev/null; then say "ccache"; ccache --show-stats | grep -iE 'hits|misses|cache size' || true; fi
+2 -2
View File
@@ -5,11 +5,11 @@
# #
# Versions match what React Native 0.86 / Expo 57 ask for # Versions match what React Native 0.86 / Expo 57 ask for
# (node_modules/react-native/gradle/libs.versions.toml). Gradle downloads anything # (node_modules/react-native/gradle/libs.versions.toml). Gradle downloads anything
# else it needs on first use; the workflow keeps ~/.gradle in a volume. # else it needs on first use; the workflow keeps ~/.gradle (and ccache's ~/.ccache) in volumes.
FROM node:22-bookworm FROM node:22-bookworm
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends openjdk-17-jdk-headless unzip \ && apt-get install -y --no-install-recommends openjdk-17-jdk-headless unzip ccache \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \ ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
+2 -1
View File
@@ -20,7 +20,8 @@ container:
# Use Docker's default network instead of creating one per job: a server with many compose # Use Docker's default network instead of creating one per job: a server with many compose
# projects runs out of address pools ("all predefined address pools have been fully subnetted"). # projects runs out of address pools ("all predefined address pools have been fully subnetted").
network: bridge network: bridge
# The volumes the workflow may mount (Gradle and npm caches), and nothing else. # The volumes the workflow may mount (Gradle, npm and ccache caches), and nothing else.
valid_volumes: valid_volumes:
- musicshare-gradle - musicshare-gradle
- musicshare-npm - musicshare-npm
- musicshare-ccache
+5 -2
View File
@@ -20,8 +20,11 @@ The pieces:
## One-time setup (on the Gitea server) ## One-time setup (on the Gitea server)
The server needs about 8 GB of free memory during a build and about 15 GB of disk for the image and The server needs about 8 GB of free memory during a build and about 20 GB of disk for the image and
caches. A first build takes 30-40 minutes; later ones reuse the Gradle cache. caches. A first build takes 15-40 minutes depending on the machine; later ones reuse the Gradle build
cache and ccache's compiled C++ (volumes `musicshare-gradle` and `musicshare-ccache`) and take a few
minutes. The APKs are for 64-bit phones (`arm64-v8a`); set `ABIS=armeabi-v7a,arm64-v8a` in the workflow's
Build step to include old 32-bit ones.
The caches are Docker volumes, which live wherever Docker keeps its data (usually `/var/lib/docker`). The caches are Docker volumes, which live wherever Docker keeps its data (usually `/var/lib/docker`).
Check that disk has room (`df -h /var/lib/docker`): if it fills up, everything else using Docker on the Check that disk has room (`df -h /var/lib/docker`): if it fills up, everything else using Docker on the
server, including databases, stops writing. server, including databases, stops writing.
@@ -0,0 +1 @@
Videos and screen recordings played on your phone no longer show up as your song, and notifications now have their own section.
+1 -1
View File
@@ -809,7 +809,7 @@
}, },
"apps/mobile": { "apps/mobile": {
"name": "@musicshare/mobile", "name": "@musicshare/mobile",
"version": "1.4.0", "version": "1.5.0",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@expo-google-fonts/poppins": "^0.4.1", "@expo-google-fonts/poppins": "^0.4.1",