Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
1ae7716
audio controller
mebtte Nov 11, 2025
f63a9ca
playqueue view
mebtte Nov 18, 2025
d6ae6af
playlist view
mebtte Nov 18, 2025
a0340da
fromJSON --> fromJson
mebtte Jan 5, 2026
ec1701f
Merge remote-tracking branch 'origin/beta' into flutter_v2
mebtte Jan 15, 2026
8d37d0d
support 2fa
mebtte Jan 19, 2026
2dc2db6
create musicbill
mebtte Jan 21, 2026
7c31237
profile and logout
mebtte Jan 21, 2026
fedd28d
home safearea
mebtte Jan 21, 2026
d995243
improve musicbill list
mebtte Jan 21, 2026
afa0f61
improve style of player controller
mebtte Jan 22, 2026
b1d3ad5
improve styles
mebtte Jan 22, 2026
601e446
improve musicbill page
mebtte Jan 23, 2026
b542edf
improve homepage
mebtte Jan 23, 2026
f0d82fc
rotate cover while playing
mebtte Jan 26, 2026
e278e7f
search page
mebtte Jan 26, 2026
54fb50e
music search
mebtte Jan 26, 2026
6b5f1af
lyric search
mebtte Jan 26, 2026
1693167
improve search page
mebtte Jan 26, 2026
195485b
upload music play record
mebtte Jan 27, 2026
7290df4
play indicator
mebtte Jan 27, 2026
654a47c
search intermeidate
mebtte Jan 27, 2026
a4b01d9
improve search intermediate
mebtte Jan 27, 2026
686435e
error display
mebtte Jan 28, 2026
6d9f650
player page
mebtte Jan 28, 2026
f0c5b70
improve player
mebtte Jan 28, 2026
a777c08
auto play next after error
mebtte Feb 1, 2026
b31963c
improve player
mebtte Feb 1, 2026
de93de5
improve player-error
mebtte Feb 1, 2026
391bdef
improve play option
mebtte Feb 2, 2026
600cede
improve playlist/playqueue
mebtte Feb 2, 2026
d359e91
fix playqueue updating
mebtte Feb 3, 2026
96c4fc6
improve playqueue
mebtte Feb 3, 2026
f0fcf37
improve playqueue
mebtte Feb 3, 2026
7774695
add version display
mebtte Feb 5, 2026
a253957
improve app version
mebtte Feb 5, 2026
af21fb4
android support
mebtte Feb 9, 2026
525c465
timeout as play-error
mebtte Feb 9, 2026
cb9a21c
add image cache
mebtte Feb 9, 2026
8776cdd
add music cache
mebtte Feb 9, 2026
2874364
fix add-all-to-playlist
mebtte Feb 9, 2026
da41341
flutter upgrade
mebtte Mar 8, 2026
4fa165f
add agents.md
mebtte Mar 17, 2026
8ebcbad
update AGENTS.md
mebtte Mar 18, 2026
2a82eaf
add frequence limit to login
mebtte Mar 18, 2026
28962a5
loading for update
mebtte Mar 18, 2026
5f921b8
auto next volume
mebtte Mar 27, 2026
adf5bd5
fix auto next mistakenly
mebtte Mar 27, 2026
b28dc20
add music to musicbill
mebtte Mar 31, 2026
a11333d
still playing when inserting music to playqueue
mebtte Mar 31, 2026
6307728
fix update_fork_from
mebtte Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["dart-code.dart-code", "dart-code.flutter"]
}
11 changes: 10 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Debug server",
"name": "Server",
"request": "launch",
"type": "node",
"runtimeArgs": [
Expand All @@ -16,6 +16,15 @@
"env": {
"TS_NODE_PROJECT": "${workspaceFolder}/apps/cli/tsconfig.json"
}
},
{
"name": "Flutter - macOS",
"request": "launch",
"type": "dart",
"program": "${workspaceFolder}/apps/flutter/lib/main.dart",
"cwd": "${workspaceFolder}/apps/flutter",
"deviceId": "macos",
"flutterMode": "debug"
}
]
}
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# AGENTS.md

## Overview

Cicada is a multi-user music service for self-hosting, and it has three apps:

- cli: it uses for starting a server and managing the data and it is powered by node.js
- flutter: it's a client for cross-platform and it is powered by flutter
- pwa: it's a client for web and it is powered by react

## Rules

- Variants prefer lower camel case
- Filenames prefer snake case
7 changes: 7 additions & 0 deletions apps/cli/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# AGENTS.md

`cli` is a terminal tool to start a server and manage the data.

## Overview

- use `sqlite` as database
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import { getMusicListByIds } from '@/db/music';
import { MusicForkProperty, MusicProperty } from '@/constants/db_definition';
import { Parameter } from './constants';

function isString(s: unknown): s is string {
return typeof s === 'string';
}

export default async ({ ctx, music, value }: Parameter) => {
if (
!Array.isArray(value) ||
value.length > 100 ||
value.find((v) => typeof v !== 'string') ||
!value.every(isString) ||
value.find((v) => v === music.id)
) {
return ctx.error(ExceptionCode.WRONG_PARAMETER);
Expand All @@ -31,9 +35,11 @@ export default async ({ ctx, music, value }: Parameter) => {
return ctx.except(ExceptionCode.NO_NEED_TO_UPDATE);
}

const musicList = await getMusicListByIds(value, [MusicProperty.ID]);
if (musicList.length !== value.length) {
return ctx.except(ExceptionCode.MUSIC_NOT_EXISTED);
if (value.length) {
const musicList = await getMusicListByIds(value, [MusicProperty.ID]);
if (musicList.length !== value.length) {
return ctx.except(ExceptionCode.MUSIC_NOT_EXISTED);
}
}

await Promise.all([
Expand All @@ -44,13 +50,15 @@ export default async ({ ctx, music, value }: Parameter) => {
`,
oldForkFromList.map((f) => f.id),
),
getDB().run(
`
INSERT INTO music_fork ( musicId, forkFrom )
VALUES ${value.map(() => '( ?, ? )').join(', ')}
`,
value.map((v) => [music.id, v]).flat(),
),
value.length
? getDB().run(
`
INSERT INTO music_fork ( musicId, forkFrom )
VALUES ${value.map(() => '( ?, ? )').join(', ')}
`,
value.map((v) => [music.id, v]).flat(),
)
: null,
saveMusicModifyRecord({
musicId: music.id,
key: AllowUpdateKey.FORK_FROM,
Expand Down
22 changes: 22 additions & 0 deletions apps/cli/src/commands/start_server/base_app/controllers/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ import { UNUSED_2FA_SECRET_PREFIX } from '@/constants';
import * as captcha from '@/platform/captcha';
import { Context } from '../constants';

const USER_LOGIN_INTERVAL = 3000;
const userLastLoginTime = new Map<string, number>();

function clearExpiredUserLastLoginTime(now: number) {
for (const [username, lastLoginTime] of userLastLoginTime) {
if (now - lastLoginTime >= USER_LOGIN_INTERVAL) {
userLastLoginTime.delete(username);
}
}
}

export default async (ctx: Context) => {
const { username, password, captchaId, captchaValue } = ctx.request.body as {
[key in keyof RequestBody]: unknown;
Expand All @@ -26,6 +37,17 @@ export default async (ctx: Context) => {
return ctx.except(ExceptionCode.WRONG_PARAMETER);
}

const now = Date.now();
clearExpiredUserLastLoginTime(now);
const lastLoginTime = userLastLoginTime.get(username);
if (
typeof lastLoginTime === 'number' &&
now - lastLoginTime < USER_LOGIN_INTERVAL
) {
return ctx.except(ExceptionCode.LOGIN_TOO_FREQUENT);
}
userLastLoginTime.set(username, now);

const captchaVerified = await captcha.verify({
id: captchaId,
value: captchaValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ import { UNUSED_2FA_SECRET_PREFIX } from '@/constants';
import * as twoFA from '@/platform/2fa';
import { Context } from '../constants';

const USER_LOGIN_WITH_2FA_INTERVAL = 3000;
const userLastLoginWith2FATime = new Map<string, number>();

function clearExpiredUserLastLoginWith2FATime(now: number) {
for (const [username, lastLoginWith2FATime] of userLastLoginWith2FATime) {
if (now - lastLoginWith2FATime >= USER_LOGIN_WITH_2FA_INTERVAL) {
userLastLoginWith2FATime.delete(username);
}
}
}

export default async (ctx: Context) => {
const { username, password, twoFAToken } = ctx.request.body as {
[key in keyof RequestBody]: unknown;
Expand All @@ -24,6 +35,17 @@ export default async (ctx: Context) => {
return ctx.except(ExceptionCode.WRONG_PARAMETER);
}

const now = Date.now();
clearExpiredUserLastLoginWith2FATime(now);
const lastLoginWith2FATime = userLastLoginWith2FATime.get(username);
if (
typeof lastLoginWith2FATime === 'number' &&
now - lastLoginWith2FATime < USER_LOGIN_WITH_2FA_INTERVAL
) {
return ctx.except(ExceptionCode.LOGIN_WITH_2FA_TOO_FREQUENT);
}
userLastLoginWith2FATime.set(username, now);

const user = await getUserByUsername(username, [
UserProperty.ID,
UserProperty.PASSWORD,
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/commands/start_server/constants/exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ export const EXCEPTION_CODE_MAP_KEY: Record<ExceptionCode, Key> = {
[ExceptionCode.TWO_FA_ENABLED_ALREADY]: '2fa_enabled_already',
[ExceptionCode.NEED_2FA]: 'need_2fa',
[ExceptionCode.NO_NEED_TO_2FA]: 'no_need_to_2fa',
[ExceptionCode.LOGIN_TOO_FREQUENT]: 'login_too_frequent',
[ExceptionCode.LOGIN_WITH_2FA_TOO_FREQUENT]: 'login_with_2fa_too_frequent',
};
2 changes: 2 additions & 0 deletions apps/cli/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ export default {
'2fa_enabled_already': '2FA enabled already',
need_2fa: 'need 2FA',
no_need_to_2fa: 'no need to 2FA',
login_too_frequent: 'login too frequent',
login_with_2fa_too_frequent: 'login with 2FA too frequent',
};
2 changes: 2 additions & 0 deletions apps/cli/src/i18n/zh_hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const zhHans: {
'2fa_enabled_already': '2FA 早已启用',
need_2fa: '需要 2FA',
no_need_to_2fa: '无需 2FA',
login_too_frequent: 'login 请求过于频繁',
login_with_2fa_too_frequent: 'login_with_2fa 请求过于频繁',
};

export default zhHans;
1 change: 1 addition & 0 deletions apps/flutter/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# AGENTS.md
27 changes: 26 additions & 1 deletion apps/flutter/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<application
android:label="cicada"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
Expand All @@ -25,6 +32,24 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- audio_service -->
<service
android:name="com.ryanheise.audioservice.AudioService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>

<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.mebtte.cicada

import io.flutter.embedding.android.FlutterActivity
import com.ryanheise.audioservice.AudioServiceActivity

class MainActivity : FlutterActivity()
class MainActivity : AudioServiceActivity()
2 changes: 0 additions & 2 deletions apps/flutter/ios/Flutter/AppFrameworkInfo.plist
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
13 changes: 6 additions & 7 deletions apps/flutter/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ PODS:
- just_audio (0.0.1):
- Flutter
- FlutterMacOS
- path_provider_foundation (0.0.1):
- permission_handler_apple (9.3.0):
- Flutter
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
Expand All @@ -23,7 +22,7 @@ DEPENDENCIES:
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- Flutter (from `Flutter`)
- just_audio (from `.symlinks/plugins/just_audio/darwin`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)

Expand All @@ -36,8 +35,8 @@ EXTERNAL SOURCES:
:path: Flutter
just_audio:
:path: ".symlinks/plugins/just_audio/darwin"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
Expand All @@ -48,8 +47,8 @@ SPEC CHECKSUMS:
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0

PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
Expand Down
18 changes: 18 additions & 0 deletions apps/flutter/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
AA1627D9F4559A7406269F89 /* [CP] Embed Pods Frameworks */,
CAC0CCFCDB6F83242364ABE1 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
Expand Down Expand Up @@ -362,6 +363,23 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
CAC0CCFCDB6F83242364ABE1 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
Expand Down
7 changes: 5 additions & 2 deletions apps/flutter/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
Loading
Loading