@audiofool/moondog 0.2.2 → 0.3.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +12 -0
- package/README.md +4 -2
- package/licenses/stability-ai-community-license.md +58 -0
- package/package.json +3 -1
- package/scripts/moondog.mjs +171 -7
- package/src/core/apple-projection-domain-services.mjs +14 -0
- package/src/core/capability-catalog.mjs +104 -0
- package/src/core/library-store.mjs +500 -0
- package/src/core/listening-now.mjs +80 -6
- package/src/core/listening-profile-domain-services.mjs +12 -0
- package/src/core/moondog-application.mjs +754 -23
- package/src/i18n/index.mjs +6 -3
- package/src/i18n/messages/de.mjs +3 -0
- package/src/i18n/messages/en.mjs +4 -0
- package/src/i18n/messages/es.mjs +3 -0
- package/src/i18n/messages/fr.mjs +3 -0
- package/src/i18n/messages/it.mjs +3 -0
- package/src/i18n/messages/ja.mjs +3 -0
- package/src/i18n/messages/ko.mjs +3 -0
- package/src/i18n/messages/pt.mjs +3 -0
- package/src/i18n/messages/zh-Hant.mjs +3 -0
- package/src/i18n/messages/zh.mjs +3 -0
- package/src/i18n/preferences.mjs +24 -0
- package/src/i18n/screen/es.mjs +118 -0
- package/src/i18n/screen/ja.mjs +118 -0
- package/src/i18n/screen/pt.mjs +118 -0
- package/src/i18n/screen/zh.mjs +118 -0
- package/src/integrations/apple-music/music-app.mjs +9 -3
- package/src/integrations/apple-music/terminal-preview-player.mjs +68 -11
- package/src/integrations/netease/ncm-cli-player.mjs +12 -6
- package/src/integrations/playlist-links.mjs +96 -0
- package/src/integrations/public-playlists.mjs +10 -4
- package/src/integrations/spotify/service.mjs +22 -1
- package/src/integrations/stable-audio/cover.mjs +324 -0
- package/src/integrations/stable-audio/local-generator.mjs +855 -0
- package/src/integrations/stable-audio/setup.mjs +653 -0
- package/src/integrations/stable-audio/sources.mjs +106 -0
- package/src/profile/listening-history-store.mjs +1 -1
- package/src/profile/listening-profile-projection.mjs +1 -1
- package/src/profile/moondog-suggestions.mjs +9 -0
- package/src/profile/music-import-bundle.mjs +3 -1
- package/src/profile/music-providers.mjs +3 -0
- package/src/profile/taste-compiler.mjs +206 -0
- package/src/runtime/pi/agent-runtime.mjs +600 -38
- package/src/runtime/pi/model-switch.mjs +26 -0
- package/src/runtime/pi/model-transport.mjs +34 -4
- package/src/runtime/pi/record-name.mjs +80 -0
- package/src/runtime/pi/reply-language.mjs +45 -0
- package/src/runtime/pi/tool-arguments.mjs +89 -0
- package/src/surfaces/cli/command-completions.mjs +15 -0
- package/src/surfaces/cli/format-output.mjs +1 -0
- package/src/surfaces/cli/tui.mjs +467 -19
package/src/i18n/screen/pt.mjs
CHANGED
|
@@ -874,6 +874,7 @@ export default {
|
|
|
874
874
|
"See what you're listening to": "Ver o que você está ouvindo",
|
|
875
875
|
"Control the Music app": "Controlar o app Música",
|
|
876
876
|
"Choose a music service": "Escolher um serviço de música",
|
|
877
|
+
"Read or change Moondog's settings": "Ver ou mudar as configurações do Moondog",
|
|
877
878
|
"Search outside your library": "Buscar fora da sua biblioteca",
|
|
878
879
|
"Find music nearby": "Encontrar música parecida",
|
|
879
880
|
"Remember what you've told me": "Lembrar o que você me disse",
|
|
@@ -1027,4 +1028,121 @@ export default {
|
|
|
1027
1028
|
"Skipped. /service and /model are here when you want them.": "Pulado. /service e /model continuam aqui quando você quiser.",
|
|
1028
1029
|
"Saving your music service...": "Salvando seu serviço de música...",
|
|
1029
1030
|
"Choose where Moondog plays music": "Escolha onde o Moondog toca música",
|
|
1031
|
+
"Make music on this Mac with Stable Audio 3": "Fazer música neste Mac com o Stable Audio 3",
|
|
1032
|
+
"Stop making music here": "Parar de fazer música aqui",
|
|
1033
|
+
"Making music on this Mac": "Fazer música neste Mac",
|
|
1034
|
+
"Made by Moondog": "Feito pelo Moondog",
|
|
1035
|
+
"Stable Audio 3 needs a Mac with Apple silicon, so Moondog can't make music here.": "O Stable Audio 3 precisa de um Mac com Apple silicon, então o Moondog não pode fazer música aqui.",
|
|
1036
|
+
"Moondog makes music on this Mac with Stable Audio 3, powered by Stability AI. Ask for a track. Stable Audio 3 runs offline, and the track stays on this Mac.": "O Moondog faz música neste Mac com o Stable Audio 3, powered by Stability AI. Peça uma faixa. O Stable Audio 3 funciona offline, e a faixa fica neste Mac.",
|
|
1037
|
+
"Making music on this Mac is off. Stable Audio 3 is ready at {folder}; turn it on with /generation on.": "Fazer música neste Mac está desativado. O Stable Audio 3 está pronto em {folder}; ative com /generation on.",
|
|
1038
|
+
"Making music on this Mac is off, and Stable Audio 3 isn't at {folder}. Install Stability AI's MLX build from github.com/Stability-AI/stable-audio-3, then use /generation on with its folder.": "Fazer música neste Mac está desativado, e o Stable Audio 3 não está em {folder}. Instale a versão MLX da Stability AI a partir de github.com/Stability-AI/stable-audio-3 e depois use /generation on com a pasta dela.",
|
|
1039
|
+
"Making music on this Mac is off, and Stable Audio 3 at {folder} isn't fully installed. Run ./install.sh --download medium there, then use /generation on.": "Fazer música neste Mac está desativado, e o Stable Audio 3 em {folder} não está totalmente instalado. Execute ./install.sh --download medium lá e depois use /generation on.",
|
|
1040
|
+
"Stable Audio 3 at {folder} doesn't have the Medium model yet. Run ./install.sh --download medium there, then use /generation again.": "O Stable Audio 3 em {folder} ainda não tem o modelo Medium. Execute ./install.sh --download medium lá e use /generation de novo.",
|
|
1041
|
+
"Stable Audio 3 at {folder} isn't installed yet. Run ./install.sh --download medium there, then use /generation again.": "O Stable Audio 3 em {folder} ainda não está instalado. Execute ./install.sh --download medium lá e use /generation de novo.",
|
|
1042
|
+
"Stable Audio 3 isn't at {folder}. Use /generation on with the folder that holds its sa3 script.": "O Stable Audio 3 não está em {folder}. Use /generation on com a pasta que contém o script sa3.",
|
|
1043
|
+
"Use /generation, /generation on [folder], or /generation off.": "Use /generation, /generation on [pasta] ou /generation off.",
|
|
1044
|
+
"Music making on.": "Criação de música ativada.",
|
|
1045
|
+
"Music making off.": "Criação de música desativada.",
|
|
1046
|
+
"Music making isn't ready.": "A criação de música não está pronta.",
|
|
1047
|
+
"warming up": "aquecendo",
|
|
1048
|
+
"reading your taste": "lendo seu gosto",
|
|
1049
|
+
"composing": "compondo",
|
|
1050
|
+
"pressing the record": "prensando o disco",
|
|
1051
|
+
"saving it": "salvando",
|
|
1052
|
+
"Let Moondog make music on this Mac": "Deixe o Moondog fazer música neste Mac",
|
|
1053
|
+
"Make a track": "Fazer uma faixa",
|
|
1054
|
+
"Play a track Moondog made": "Tocar de novo uma faixa que o Moondog fez",
|
|
1055
|
+
"Start a stream": "Começar um fluxo",
|
|
1056
|
+
"Stop the stream": "Parar o fluxo",
|
|
1057
|
+
"Go on air: Moondog keeps making music from your taste": "Entre no ar: o Moondog segue fazendo música a partir do seu gosto",
|
|
1058
|
+
"on air until {time}": "no ar até {time}",
|
|
1059
|
+
"Cutting the next record": "Gravando o próximo disco",
|
|
1060
|
+
"Off air: the next record couldn't be made.": "Fora do ar: não deu para fazer o próximo disco.",
|
|
1061
|
+
"The next record is still being cut.": "O próximo disco ainda está sendo gravado.",
|
|
1062
|
+
"Off air.": "Fora do ar.",
|
|
1063
|
+
"Turn on music making first: /generation on.": "Ative a criação de música primeiro: /generation on.",
|
|
1064
|
+
"Download a Stable Audio 3 model from Stability AI": "Baixar um modelo do Stable Audio 3 da Stability AI",
|
|
1065
|
+
"Make music with a downloaded model": "Fazer música com um modelo baixado",
|
|
1066
|
+
"Delete a downloaded model from this Mac": "Apagar deste Mac um modelo baixado",
|
|
1067
|
+
"Stop the download": "Parar o download",
|
|
1068
|
+
"Medium: best sound, about 5.2 GB": "Medium: o melhor som, cerca de 5,2 GB",
|
|
1069
|
+
"Small Music: faster, lighter, about 1.7 GB": "Small Music: mais rápido e leve, cerca de 1,7 GB",
|
|
1070
|
+
"getting ready": "preparando",
|
|
1071
|
+
"There isn't enough free space on this Mac for the download. Free some up, then use /generation download again.": "Não há espaço livre suficiente neste Mac para o download. Libere um pouco e use /generation download de novo.",
|
|
1072
|
+
"A downloaded file didn't match the one Stability AI publishes, so Moondog deleted it. Use /generation download to try again.": "Um arquivo baixado não coincidia com o que a Stability AI publica, então o Moondog o apagou. Use /generation download para tentar de novo.",
|
|
1073
|
+
"The download couldn't reach its source. Check your connection, then use /generation download to try again.": "O download não conseguiu chegar à origem. Verifique sua conexão e use /generation download para tentar de novo.",
|
|
1074
|
+
"Another Moondog window or terminal is already downloading. /generation shows how far it is.": "Outra janela ou terminal do Moondog já está baixando. /generation mostra quanto falta.",
|
|
1075
|
+
"The download didn't finish: {error}. Use /generation download to try again.": "O download não terminou: {error}. Use /generation download para tentar de novo.",
|
|
1076
|
+
"unknown error": "erro desconhecido",
|
|
1077
|
+
"Stable Audio 3 {model} is ready. Ask for a track.": "O Stable Audio 3 {model} está pronto. Peça uma faixa.",
|
|
1078
|
+
"Stable Audio 3 {model} is downloaded. Use it with /generation use {choice}.": "O Stable Audio 3 {model} foi baixado. Use com /generation use {choice}.",
|
|
1079
|
+
"Download stopped. /generation download {choice} picks it up where it left off.": "Download parado. /generation download {choice} continua de onde parou.",
|
|
1080
|
+
"download stopped on an error": "download parado por um erro",
|
|
1081
|
+
"downloading": "baixando",
|
|
1082
|
+
"downloading, {percent}%": "baixando, {percent}%",
|
|
1083
|
+
"installed, in use": "instalado, em uso",
|
|
1084
|
+
"installed": "instalado",
|
|
1085
|
+
"partly downloaded": "baixado em parte",
|
|
1086
|
+
"not downloaded": "não baixado",
|
|
1087
|
+
"{size} GB": "{size} GB",
|
|
1088
|
+
"In use: your own install at {folder}.": "Em uso: sua própria instalação em {folder}.",
|
|
1089
|
+
"Get a model with /generation download medium or small. /generation use, remove and cancel manage them.": "Baixe um modelo com /generation download medium ou small. /generation use, remove e cancel cuidam deles.",
|
|
1090
|
+
"Making music on this Mac is off. Get Stable Audio 3 with /generation download medium, or small for a lighter model; music making turns on when it's done.": "Fazer música neste Mac está desativado. Baixe o Stable Audio 3 com /generation download medium, ou small para um modelo mais leve; a criação de música é ativada ao terminar.",
|
|
1091
|
+
"Stable Audio 3 {model} isn't downloaded yet. Get it with /generation download {choice}.": "O Stable Audio 3 {model} ainda não foi baixado. Baixe com /generation download {choice}.",
|
|
1092
|
+
"Making music on this Mac is off. Stable Audio 3 {model} is ready; turn it on with /generation on.": "Fazer música neste Mac está desativado. O Stable Audio 3 {model} está pronto; ative com /generation on.",
|
|
1093
|
+
"Use /generation download, use or remove with medium or small, or /generation cancel.": "Use /generation download, use ou remove com medium ou small, ou /generation cancel.",
|
|
1094
|
+
"No download is running.": "Nenhum download em andamento.",
|
|
1095
|
+
"Stable Audio 3 {model} is already downloading.": "O Stable Audio 3 {model} já está sendo baixado.",
|
|
1096
|
+
"Another download is running. /generation cancel stops it.": "Há outro download em andamento. /generation cancel o interrompe.",
|
|
1097
|
+
"Stable Audio 3 {model} is already on this Mac. Use it with /generation use {choice}.": "O Stable Audio 3 {model} já está neste Mac. Use com /generation use {choice}.",
|
|
1098
|
+
"Already downloaded.": "Já foi baixado.",
|
|
1099
|
+
"Stable Audio 3 comes under the Stability AI Community License ({license}), and its text encoder, T5Gemma, under the Gemma Terms of Use ({gemma}). Moondog downloads it only from Stability AI's official sources and checks every file.\n\nTo agree and download {model} ({size} GB): /generation download {choice} --agree": "O Stable Audio 3 está sob a Stability AI Community License ({license}), e seu codificador de texto, T5Gemma, sob os Gemma Terms of Use ({gemma}). O Moondog o baixa só das fontes oficiais da Stability AI e confere cada arquivo.\n\nPara aceitar e baixar o {model} ({size} GB): /generation download {choice} --agree",
|
|
1100
|
+
"Waiting for your agreement.": "Aguardando sua aceitação.",
|
|
1101
|
+
"The download didn't finish.": "O download não terminou.",
|
|
1102
|
+
"Downloading Stable Audio 3 {model} ({size} GB) from Stability AI's official sources. It runs in the background; /generation cancel stops it, and a later download picks up where it left off.": "Baixando o Stable Audio 3 {model} ({size} GB) das fontes oficiais da Stability AI. Ele segue em segundo plano; /generation cancel o interrompe, e um download depois continua de onde parou.",
|
|
1103
|
+
"Downloading Stable Audio 3 {model}.": "Baixando o Stable Audio 3 {model}.",
|
|
1104
|
+
"Wait for the download to finish, or stop it with /generation cancel, before removing a model.": "Espere o download terminar, ou pare com /generation cancel, antes de remover um modelo.",
|
|
1105
|
+
"A download is running.": "Há um download em andamento.",
|
|
1106
|
+
"Stable Audio 3 {model} isn't on this Mac.": "O Stable Audio 3 {model} não está neste Mac.",
|
|
1107
|
+
"Removing Stable Audio 3 {model} deletes its {size} GB from this Mac. It's the last model, so the rest of Moondog's Stable Audio 3 setup goes too. To go ahead: /generation remove {choice} --yes": "Remover o Stable Audio 3 {model} apaga seus {size} GB deste Mac. É o último modelo, então o resto da instalação do Stable Audio 3 do Moondog também sai. Para continuar: /generation remove {choice} --yes",
|
|
1108
|
+
"Removing Stable Audio 3 {model} deletes its {size} GB from this Mac. To go ahead: /generation remove {choice} --yes": "Remover o Stable Audio 3 {model} apaga seus {size} GB deste Mac. Para continuar: /generation remove {choice} --yes",
|
|
1109
|
+
"Nothing removed yet.": "Nada foi removido ainda.",
|
|
1110
|
+
"Removed Stable Audio 3 {model} from this Mac.": "Stable Audio 3 {model} removido deste Mac.",
|
|
1111
|
+
"Removed.": "Removido.",
|
|
1112
|
+
"Removed. Music making isn't ready.": "Removido. A criação de música não está pronta.",
|
|
1113
|
+
"Use /generation, /generation on [folder] or off, or /generation download, use or remove with medium or small.": "Use /generation, /generation on [pasta] ou off, ou /generation download, use ou remove com medium ou small.",
|
|
1114
|
+
"best sound": "o melhor som",
|
|
1115
|
+
"faster, lighter": "mais rápido e leve",
|
|
1116
|
+
"getting uv": "obtendo o uv",
|
|
1117
|
+
"getting Stable Audio 3's code": "obtendo o código do Stable Audio 3",
|
|
1118
|
+
"setting up Python": "preparando o Python",
|
|
1119
|
+
"downloading the model": "baixando o modelo",
|
|
1120
|
+
"checking the files": "conferindo os arquivos",
|
|
1121
|
+
"Stable Audio 3's Python environment couldn't be set up. Check your connection, then use /generation download to try again.": "Não foi possível preparar o ambiente Python do Stable Audio 3. Verifique sua conexão e use /generation download para tentar de novo.",
|
|
1122
|
+
"The library isn't available here.": "A biblioteca não está disponível aqui.",
|
|
1123
|
+
"Saved": "Salvas",
|
|
1124
|
+
"Play one with `/library play <name>`, or keep the song playing now with `/library save`.": "Toque uma com `/library play <nome>` ou salve o que está tocando com `/library save`.",
|
|
1125
|
+
"No playlist is called “{name}”.": "Nenhuma playlist se chama “{name}”.",
|
|
1126
|
+
"{name} has nothing in it yet.": "{name} ainda não tem nada.",
|
|
1127
|
+
"None of its songs could be found.": "Nenhuma das músicas foi encontrada.",
|
|
1128
|
+
"Playing {name}.": "Tocando {name}.",
|
|
1129
|
+
"Moondog's tracks were left out. They play with Apple Music chosen.": "As faixas do Moondog ficaram de fora. Elas tocam com o Apple Music escolhido.",
|
|
1130
|
+
"Some songs couldn't be found.": "Algumas músicas não foram encontradas.",
|
|
1131
|
+
"Already in {name}.": "Já está em {name}.",
|
|
1132
|
+
"Saved.": "Salva.",
|
|
1133
|
+
"Saved to {name}.": "Salva em {name}.",
|
|
1134
|
+
"Your saved songs and playlists": "Suas músicas salvas e playlists",
|
|
1135
|
+
"Look in the library": "Olhar na biblioteca",
|
|
1136
|
+
"Save to the library": "Salvar na biblioteca",
|
|
1137
|
+
"Play from the library": "Tocar da biblioteca",
|
|
1138
|
+
"Paste a playlist link after /library import.": "Cole um link de playlist depois de /library import.",
|
|
1139
|
+
"Reading the playlist…": "Lendo a playlist…",
|
|
1140
|
+
"{added} of {total} songs came in.": "Entraram {added} de {total} músicas.",
|
|
1141
|
+
"{count} songs came in.": { one: "Entrou {count} música.", other: "Entraram {count} músicas." },
|
|
1142
|
+
"It now shapes your taste.": "Agora ela molda seu gosto.",
|
|
1143
|
+
"It couldn't shape your taste this time.": "Desta vez não deu para incluí-la no seu gosto.",
|
|
1144
|
+
"It doesn't shape your taste; add taste after the link if it should.": "Ela não molda seu gosto; adicione taste depois do link se deveria.",
|
|
1145
|
+
"Imported {name}.": "{name} importada.",
|
|
1146
|
+
"Use /library, /library play <name>, /library save [playlist], or /library import <link>.": "Use /library, /library play <nome>, /library save [playlist] ou /library import <link>.",
|
|
1147
|
+
"Import a playlist": "Importar uma playlist",
|
|
1030
1148
|
};
|
package/src/i18n/screen/zh.mjs
CHANGED
|
@@ -874,6 +874,7 @@ export default {
|
|
|
874
874
|
"See what you're listening to": "查看你在听什么",
|
|
875
875
|
"Control the Music app": "控制“音乐”App",
|
|
876
876
|
"Choose a music service": "选择音乐服务",
|
|
877
|
+
"Read or change Moondog's settings": "读取或更改 Moondog 的设置",
|
|
877
878
|
"Search outside your library": "在你的曲库之外搜索",
|
|
878
879
|
"Find music nearby": "找附近的音乐",
|
|
879
880
|
"Remember what you've told me": "记住你告诉我的",
|
|
@@ -1027,4 +1028,121 @@ export default {
|
|
|
1027
1028
|
"Skipped. /service and /model are here when you want them.": "已跳过。需要时随时可以用 /service 和 /model。",
|
|
1028
1029
|
"Saving your music service...": "正在保存音乐服务...",
|
|
1029
1030
|
"Choose where Moondog plays music": "选择 Moondog 在哪里放音乐",
|
|
1031
|
+
"Make music on this Mac with Stable Audio 3": "用 Stable Audio 3 在这台 Mac 上做音乐",
|
|
1032
|
+
"Stop making music here": "不再在这里做音乐",
|
|
1033
|
+
"Making music on this Mac": "在这台 Mac 上做音乐",
|
|
1034
|
+
"Made by Moondog": "Moondog 制作",
|
|
1035
|
+
"Stable Audio 3 needs a Mac with Apple silicon, so Moondog can't make music here.": "Stable Audio 3 需要搭载 Apple 芯片的 Mac,所以 Moondog 没法在这里做音乐。",
|
|
1036
|
+
"Moondog makes music on this Mac with Stable Audio 3, powered by Stability AI. Ask for a track. Stable Audio 3 runs offline, and the track stays on this Mac.": "Moondog 用这台 Mac 上的 Stable Audio 3 做音乐,Powered by Stability AI。想听就让它做一首。Stable Audio 3 离线运行,做出的曲子留在这台 Mac 上。",
|
|
1037
|
+
"Making music on this Mac is off. Stable Audio 3 is ready at {folder}; turn it on with /generation on.": "在这台 Mac 上做音乐已关闭。Stable Audio 3 已在 {folder} 准备好,用 /generation on 开启。",
|
|
1038
|
+
"Making music on this Mac is off, and Stable Audio 3 isn't at {folder}. Install Stability AI's MLX build from github.com/Stability-AI/stable-audio-3, then use /generation on with its folder.": "在这台 Mac 上做音乐已关闭,{folder} 里也没有 Stable Audio 3。先从 github.com/Stability-AI/stable-audio-3 安装 Stability AI 的 MLX 版本,再用 /generation on 加上它的文件夹。",
|
|
1039
|
+
"Making music on this Mac is off, and Stable Audio 3 at {folder} isn't fully installed. Run ./install.sh --download medium there, then use /generation on.": "在这台 Mac 上做音乐已关闭,{folder} 里的 Stable Audio 3 也还没装完整。在那里运行 ./install.sh --download medium,然后用 /generation on。",
|
|
1040
|
+
"Stable Audio 3 at {folder} doesn't have the Medium model yet. Run ./install.sh --download medium there, then use /generation again.": "{folder} 里的 Stable Audio 3 还没有 Medium 模型。在那里运行 ./install.sh --download medium,然后再用一次 /generation。",
|
|
1041
|
+
"Stable Audio 3 at {folder} isn't installed yet. Run ./install.sh --download medium there, then use /generation again.": "{folder} 里的 Stable Audio 3 还没装好。在那里运行 ./install.sh --download medium,然后再用一次 /generation。",
|
|
1042
|
+
"Stable Audio 3 isn't at {folder}. Use /generation on with the folder that holds its sa3 script.": "{folder} 里没有 Stable Audio 3。用 /generation on 加上包含 sa3 脚本的文件夹。",
|
|
1043
|
+
"Use /generation, /generation on [folder], or /generation off.": "用 /generation、/generation on [文件夹] 或 /generation off。",
|
|
1044
|
+
"Music making on.": "已开启音乐生成。",
|
|
1045
|
+
"Music making off.": "已关闭音乐生成。",
|
|
1046
|
+
"Music making isn't ready.": "音乐生成还没准备好。",
|
|
1047
|
+
"warming up": "热身中",
|
|
1048
|
+
"reading your taste": "读你的口味",
|
|
1049
|
+
"composing": "作曲中",
|
|
1050
|
+
"pressing the record": "压制唱片",
|
|
1051
|
+
"saving it": "保存中",
|
|
1052
|
+
"Let Moondog make music on this Mac": "让 Moondog 在这台 Mac 上做音乐",
|
|
1053
|
+
"Make a track": "做一首曲子",
|
|
1054
|
+
"Play a track Moondog made": "再放一首 Moondog 做的曲子",
|
|
1055
|
+
"Start a stream": "开始音乐流",
|
|
1056
|
+
"Stop the stream": "停止音乐流",
|
|
1057
|
+
"Go on air: Moondog keeps making music from your taste": "开播:Moondog 照着你的口味一直做音乐",
|
|
1058
|
+
"on air until {time}": "播出到 {time}",
|
|
1059
|
+
"Cutting the next record": "正在刻下一张",
|
|
1060
|
+
"Off air: the next record couldn't be made.": "停播了:下一张没刻成。",
|
|
1061
|
+
"The next record is still being cut.": "下一张还在刻。",
|
|
1062
|
+
"Off air.": "已停播。",
|
|
1063
|
+
"Turn on music making first: /generation on.": "先开启音乐生成:/generation on。",
|
|
1064
|
+
"Download a Stable Audio 3 model from Stability AI": "从 Stability AI 下载一个 Stable Audio 3 模型",
|
|
1065
|
+
"Make music with a downloaded model": "用已下载的模型做音乐",
|
|
1066
|
+
"Delete a downloaded model from this Mac": "从这台 Mac 删除一个已下载的模型",
|
|
1067
|
+
"Stop the download": "停止下载",
|
|
1068
|
+
"Medium: best sound, about 5.2 GB": "Medium:音质最好,约 5.2 GB",
|
|
1069
|
+
"Small Music: faster, lighter, about 1.7 GB": "Small Music:更快更轻,约 1.7 GB",
|
|
1070
|
+
"getting ready": "准备中",
|
|
1071
|
+
"There isn't enough free space on this Mac for the download. Free some up, then use /generation download again.": "这台 Mac 的可用空间不够下载。腾出一些空间,再用一次 /generation download。",
|
|
1072
|
+
"A downloaded file didn't match the one Stability AI publishes, so Moondog deleted it. Use /generation download to try again.": "有个下载的文件和 Stability AI 发布的不一致,Moondog 已把它删除。用 /generation download 重试。",
|
|
1073
|
+
"The download couldn't reach its source. Check your connection, then use /generation download to try again.": "下载连不上来源。检查网络连接,再用 /generation download 重试。",
|
|
1074
|
+
"Another Moondog window or terminal is already downloading. /generation shows how far it is.": "另一个 Moondog 窗口或终端已经在下载。/generation 可以看进度。",
|
|
1075
|
+
"The download didn't finish: {error}. Use /generation download to try again.": "下载没有完成:{error}。用 /generation download 重试。",
|
|
1076
|
+
"unknown error": "未知错误",
|
|
1077
|
+
"Stable Audio 3 {model} is ready. Ask for a track.": "Stable Audio 3 {model} 已就绪。想听就让它做一首。",
|
|
1078
|
+
"Stable Audio 3 {model} is downloaded. Use it with /generation use {choice}.": "Stable Audio 3 {model} 已下载。用 /generation use {choice} 来使用它。",
|
|
1079
|
+
"Download stopped. /generation download {choice} picks it up where it left off.": "下载已停止。/generation download {choice} 会从停下的地方接着下。",
|
|
1080
|
+
"download stopped on an error": "下载因错误停止",
|
|
1081
|
+
"downloading": "下载中",
|
|
1082
|
+
"downloading, {percent}%": "下载中,{percent}%",
|
|
1083
|
+
"installed, in use": "已安装,使用中",
|
|
1084
|
+
"installed": "已安装",
|
|
1085
|
+
"partly downloaded": "已下载一部分",
|
|
1086
|
+
"not downloaded": "未下载",
|
|
1087
|
+
"{size} GB": "{size} GB",
|
|
1088
|
+
"In use: your own install at {folder}.": "使用中:你自己在 {folder} 的安装。",
|
|
1089
|
+
"Get a model with /generation download medium or small. /generation use, remove and cancel manage them.": "用 /generation download medium 或 small 获取模型。/generation use、remove 和 cancel 用来管理它们。",
|
|
1090
|
+
"Making music on this Mac is off. Get Stable Audio 3 with /generation download medium, or small for a lighter model; music making turns on when it's done.": "在这台 Mac 上做音乐已关闭。用 /generation download medium 获取 Stable Audio 3,想要更轻的模型就用 small;下载完成后会开启音乐生成。",
|
|
1091
|
+
"Stable Audio 3 {model} isn't downloaded yet. Get it with /generation download {choice}.": "Stable Audio 3 {model} 还没下载。用 /generation download {choice} 获取。",
|
|
1092
|
+
"Making music on this Mac is off. Stable Audio 3 {model} is ready; turn it on with /generation on.": "在这台 Mac 上做音乐已关闭。Stable Audio 3 {model} 已准备好,用 /generation on 开启。",
|
|
1093
|
+
"Use /generation download, use or remove with medium or small, or /generation cancel.": "用 /generation download、use 或 remove 加上 medium 或 small,或用 /generation cancel。",
|
|
1094
|
+
"No download is running.": "没有正在进行的下载。",
|
|
1095
|
+
"Stable Audio 3 {model} is already downloading.": "Stable Audio 3 {model} 已经在下载了。",
|
|
1096
|
+
"Another download is running. /generation cancel stops it.": "另一个下载正在进行。/generation cancel 可以停止它。",
|
|
1097
|
+
"Stable Audio 3 {model} is already on this Mac. Use it with /generation use {choice}.": "Stable Audio 3 {model} 已经在这台 Mac 上。用 /generation use {choice} 来使用它。",
|
|
1098
|
+
"Already downloaded.": "已经下载过了。",
|
|
1099
|
+
"Stable Audio 3 comes under the Stability AI Community License ({license}), and its text encoder, T5Gemma, under the Gemma Terms of Use ({gemma}). Moondog downloads it only from Stability AI's official sources and checks every file.\n\nTo agree and download {model} ({size} GB): /generation download {choice} --agree": "Stable Audio 3 适用 Stability AI Community License({license}),它的文本编码器 T5Gemma 适用 Gemma Terms of Use({gemma})。Moondog 只从 Stability AI 的官方来源下载,并校验每个文件。\n\n同意并下载 {model}({size} GB):/generation download {choice} --agree",
|
|
1100
|
+
"Waiting for your agreement.": "等你同意。",
|
|
1101
|
+
"The download didn't finish.": "下载没有完成。",
|
|
1102
|
+
"Downloading Stable Audio 3 {model} ({size} GB) from Stability AI's official sources. It runs in the background; /generation cancel stops it, and a later download picks up where it left off.": "正在从 Stability AI 的官方来源下载 Stable Audio 3 {model}({size} GB)。它在后台进行;/generation cancel 可以停止,之后再下载会从停下的地方接着下。",
|
|
1103
|
+
"Downloading Stable Audio 3 {model}.": "正在下载 Stable Audio 3 {model}。",
|
|
1104
|
+
"Wait for the download to finish, or stop it with /generation cancel, before removing a model.": "删除模型前,先等下载完成,或用 /generation cancel 停止它。",
|
|
1105
|
+
"A download is running.": "有下载正在进行。",
|
|
1106
|
+
"Stable Audio 3 {model} isn't on this Mac.": "这台 Mac 上没有 Stable Audio 3 {model}。",
|
|
1107
|
+
"Removing Stable Audio 3 {model} deletes its {size} GB from this Mac. It's the last model, so the rest of Moondog's Stable Audio 3 setup goes too. To go ahead: /generation remove {choice} --yes": "删除 Stable Audio 3 {model} 会从这台 Mac 删掉它的 {size} GB。这是最后一个模型,所以 Moondog 的 Stable Audio 3 其余部分也会一并删除。确定的话:/generation remove {choice} --yes",
|
|
1108
|
+
"Removing Stable Audio 3 {model} deletes its {size} GB from this Mac. To go ahead: /generation remove {choice} --yes": "删除 Stable Audio 3 {model} 会从这台 Mac 删掉它的 {size} GB。确定的话:/generation remove {choice} --yes",
|
|
1109
|
+
"Nothing removed yet.": "还没有删除任何东西。",
|
|
1110
|
+
"Removed Stable Audio 3 {model} from this Mac.": "已从这台 Mac 删除 Stable Audio 3 {model}。",
|
|
1111
|
+
"Removed.": "已删除。",
|
|
1112
|
+
"Removed. Music making isn't ready.": "已删除。音乐生成还没准备好。",
|
|
1113
|
+
"Use /generation, /generation on [folder] or off, or /generation download, use or remove with medium or small.": "用 /generation、/generation on [文件夹] 或 off,或用 /generation download、use 或 remove 加上 medium 或 small。",
|
|
1114
|
+
"best sound": "音质最好",
|
|
1115
|
+
"faster, lighter": "更快更轻",
|
|
1116
|
+
"getting uv": "正在获取 uv",
|
|
1117
|
+
"getting Stable Audio 3's code": "正在获取 Stable Audio 3 的代码",
|
|
1118
|
+
"setting up Python": "正在准备 Python",
|
|
1119
|
+
"downloading the model": "正在下载模型",
|
|
1120
|
+
"checking the files": "正在校验文件",
|
|
1121
|
+
"Stable Audio 3's Python environment couldn't be set up. Check your connection, then use /generation download to try again.": "没能准备好 Stable Audio 3 的 Python 环境。检查网络连接,再用 /generation download 重试。",
|
|
1122
|
+
"The library isn't available here.": "这里用不了资料库。",
|
|
1123
|
+
"Saved": "收藏",
|
|
1124
|
+
"Play one with `/library play <name>`, or keep the song playing now with `/library save`.": "用 `/library play <名字>` 播放一个,或者用 `/library save` 收藏正在播放的歌。",
|
|
1125
|
+
"No playlist is called “{name}”.": "没有叫“{name}”的播放列表。",
|
|
1126
|
+
"{name} has nothing in it yet.": "{name} 里还是空的。",
|
|
1127
|
+
"None of its songs could be found.": "里面的歌都没有找到。",
|
|
1128
|
+
"Playing {name}.": "正在播放 {name}。",
|
|
1129
|
+
"Moondog's tracks were left out. They play with Apple Music chosen.": "Moondog 做的曲子没有放。选择 Apple Music 时才会播放它们。",
|
|
1130
|
+
"Some songs couldn't be found.": "有些歌没有找到。",
|
|
1131
|
+
"Already in {name}.": "已经在“{name}”里了。",
|
|
1132
|
+
"Saved.": "已收藏。",
|
|
1133
|
+
"Saved to {name}.": "已收藏到“{name}”。",
|
|
1134
|
+
"Your saved songs and playlists": "你收藏的歌和播放列表",
|
|
1135
|
+
"Look in the library": "翻翻资料库",
|
|
1136
|
+
"Save to the library": "收进资料库",
|
|
1137
|
+
"Play from the library": "播放资料库里的歌",
|
|
1138
|
+
"Paste a playlist link after /library import.": "在 /library import 后面粘贴歌单链接。",
|
|
1139
|
+
"Reading the playlist…": "正在读取歌单…",
|
|
1140
|
+
"{added} of {total} songs came in.": "{total} 首中导入了 {added} 首。",
|
|
1141
|
+
"{count} songs came in.": { one: "导入了 {count} 首。", other: "导入了 {count} 首。" },
|
|
1142
|
+
"It now shapes your taste.": "它现在会影响你的口味画像。",
|
|
1143
|
+
"It couldn't shape your taste this time.": "这次没能把它加入你的口味画像。",
|
|
1144
|
+
"It doesn't shape your taste; add taste after the link if it should.": "它不会影响你的口味;如果需要,在链接后加 taste。",
|
|
1145
|
+
"Imported {name}.": "已导入“{name}”。",
|
|
1146
|
+
"Use /library, /library play <name>, /library save [playlist], or /library import <link>.": "用 /library、/library play <名字>、/library save [播放列表] 或 /library import <链接>。",
|
|
1147
|
+
"Import a playlist": "导入歌单",
|
|
1030
1148
|
};
|
|
@@ -12,6 +12,8 @@ const READ_TIMEOUT_MS = 15_000;
|
|
|
12
12
|
// A playlist longer than this is read as a window around the playing song.
|
|
13
13
|
const PLAYLIST_WINDOW = 50;
|
|
14
14
|
const PLAYLIST_BEFORE = 5;
|
|
15
|
+
// Keeping the rest of a playlist reads up to as many songs as a library playlist holds.
|
|
16
|
+
const REST_MAX = 2_000;
|
|
15
17
|
const COLUMN_READ_MAX = 2_000;
|
|
16
18
|
// Covers already drawn, by album, or by song for one without an album.
|
|
17
19
|
const KNOWN_COVERS = 40;
|
|
@@ -47,13 +49,15 @@ function run(argv) {
|
|
|
47
49
|
const playlist = music.currentPlaylist;
|
|
48
50
|
answer.playlist = { id: playlist.persistentID(), name: playlist.name() };
|
|
49
51
|
try { answer.playlist.special = playlist.specialKind(); } catch (error) {}
|
|
50
|
-
if (action === "tracks") {
|
|
52
|
+
if (action === "tracks" || action === "rest") {
|
|
51
53
|
const tracks = playlist.tracks;
|
|
52
54
|
const ids = tracks.persistentID();
|
|
53
55
|
answer.playlist.count = ids.length;
|
|
54
56
|
const at = answer.track ? ids.indexOf(answer.track.id) : -1;
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
+
// "rest" reads from the playing song to the playlist's end, for keeping it in the library.
|
|
58
|
+
const start = action === "rest" ? Math.max(0, at)
|
|
59
|
+
: ids.length <= ${PLAYLIST_WINDOW} || at < 0 ? 0 : Math.max(0, Math.min(at - ${PLAYLIST_BEFORE}, ids.length - ${PLAYLIST_WINDOW}));
|
|
60
|
+
const end = Math.min(ids.length, start + (action === "rest" ? ${REST_MAX} : ${PLAYLIST_WINDOW}));
|
|
57
61
|
answer.playlist.index = at;
|
|
58
62
|
answer.playlist.start = start;
|
|
59
63
|
answer.playlist.tracks = [];
|
|
@@ -197,6 +201,8 @@ export function createMusicAppPlayer({ platform = process.platform, run = osascr
|
|
|
197
201
|
return {
|
|
198
202
|
available,
|
|
199
203
|
nowPlaying: ({ tracks = false } = {}) => read(tracks ? "tracks" : "read"),
|
|
204
|
+
// The playing song and the rest of its playlist, past the window nowPlaying reads, for keeping in the library.
|
|
205
|
+
remainingPlaylist: () => read("rest"),
|
|
200
206
|
pause: () => read("pause"),
|
|
201
207
|
resume: () => read("resume"),
|
|
202
208
|
next: () => read("next"),
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// Plays the agent's Apple Music previews from the terminal. Each 30-second preview is
|
|
2
2
|
// downloaded to a temporary file and played by the system's own player, one after another.
|
|
3
|
+
// A track Moondog made on this Mac plays the same way, from its own folder.
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
4
5
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
6
|
import { access, constants, rm, stat, writeFile } from "node:fs/promises";
|
|
6
7
|
import { tmpdir } from "node:os";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
|
|
10
|
+
import { createPlayerNow } from "../../core/listening-now.mjs";
|
|
11
|
+
|
|
9
12
|
// Players that play one file without a window and exit at its end, in order of preference.
|
|
10
13
|
// None of them may read or write the terminal: the listening room owns it.
|
|
11
14
|
const systemPlayers = {
|
|
@@ -16,6 +19,8 @@ const systemPlayers = {
|
|
|
16
19
|
],
|
|
17
20
|
};
|
|
18
21
|
const longestQueue = 30;
|
|
22
|
+
// A library playlist plays as many entries as the application sends it, as the window does.
|
|
23
|
+
const longestPlaylist = 50;
|
|
19
24
|
const largestPreview = 10 * 1024 * 1024;
|
|
20
25
|
// Some routes to Apple's audio host take over 20 seconds for one preview.
|
|
21
26
|
const downloadTimeoutMs = 60_000;
|
|
@@ -68,8 +73,11 @@ export async function createTerminalPreviewPlayer({
|
|
|
68
73
|
let queue = null;
|
|
69
74
|
let child = null;
|
|
70
75
|
let closed = false;
|
|
76
|
+
// What it plays, for the agent: the queue it holds, moved along by its own events.
|
|
77
|
+
const now = createPlayerNow();
|
|
71
78
|
|
|
72
79
|
const emit = (event) => {
|
|
80
|
+
now.observe(event);
|
|
73
81
|
for (const listener of listeners) {
|
|
74
82
|
try { listener(event); } catch {}
|
|
75
83
|
}
|
|
@@ -102,22 +110,24 @@ export async function createTerminalPreviewPlayer({
|
|
|
102
110
|
return file;
|
|
103
111
|
};
|
|
104
112
|
|
|
113
|
+
// Resolves "ended" when the song played to its end, "stopped" when /skip or /stop ended it, and
|
|
114
|
+
// "failed" when the player could not play it (no audio device, an unreadable file).
|
|
105
115
|
const playFile = (file) => new Promise((resolve) => {
|
|
106
116
|
const playing = spawn(player.command, [...player.args, file], { stdio: "ignore" });
|
|
107
117
|
child = playing;
|
|
108
|
-
const finish = () => {
|
|
118
|
+
const finish = (how) => {
|
|
109
119
|
if (child === playing) child = null;
|
|
110
|
-
resolve();
|
|
120
|
+
resolve(how);
|
|
111
121
|
};
|
|
112
|
-
playing.once("error", finish);
|
|
113
|
-
playing.once("exit", finish);
|
|
122
|
+
playing.once("error", () => finish("failed"));
|
|
123
|
+
playing.once("exit", (code, signal) => finish(code === 0 ? "ended" : signal ? "stopped" : "failed"));
|
|
114
124
|
});
|
|
115
125
|
|
|
116
126
|
const start = (tracks) => {
|
|
117
127
|
const controller = new AbortController();
|
|
118
128
|
const downloads = new Map();
|
|
119
129
|
// skip() ends the song playing, or the download of the song about to play.
|
|
120
|
-
const current = { controller, skipDownload: null };
|
|
130
|
+
const current = { controller, skipDownload: null, tracks };
|
|
121
131
|
queue = current;
|
|
122
132
|
const fetchFile = (index) => {
|
|
123
133
|
if (!downloads.has(index)) {
|
|
@@ -133,24 +143,32 @@ export async function createTerminalPreviewPlayer({
|
|
|
133
143
|
void (async () => {
|
|
134
144
|
try {
|
|
135
145
|
for (let index = 0; index < tracks.length; index += 1) {
|
|
136
|
-
const { title, artist_credit, catalog_url } = tracks[index];
|
|
137
|
-
const song = { track: { title, artist_credit, catalog_url }, position: index + 1, count: tracks.length };
|
|
138
|
-
|
|
146
|
+
const { title, artist_credit, catalog_url, generated, id } = tracks[index];
|
|
147
|
+
const song = { track: generated ? { title, artist_credit, generated: true, id } : { title, artist_credit, catalog_url }, position: index + 1, count: tracks.length };
|
|
148
|
+
// A track Moondog made plays from its own folder, which outlives the queue.
|
|
149
|
+
const pending = tracks[index].file
|
|
150
|
+
? { ready: true, file: Promise.resolve(tracks[index].file), abort: () => {}, skipped: () => false }
|
|
151
|
+
: fetchFile(index);
|
|
139
152
|
if (!pending.ready) emit({ type: "loading", ...song });
|
|
140
153
|
current.skipDownload = pending.abort;
|
|
141
154
|
const file = await pending.file;
|
|
142
155
|
current.skipDownload = null;
|
|
143
156
|
if (controller.signal.aborted) break;
|
|
144
157
|
// The next preview downloads while this one plays.
|
|
145
|
-
if (index + 1 < tracks.length) void fetchFile(index + 1);
|
|
158
|
+
if (index + 1 < tracks.length && !tracks[index + 1].file) void fetchFile(index + 1);
|
|
146
159
|
if (!file) {
|
|
147
160
|
// An error, a timeout, or an oversized file; a listener's /skip is not a failure.
|
|
148
161
|
if (!pending.skipped()) emit({ type: "failed", ...song });
|
|
149
162
|
continue;
|
|
150
163
|
}
|
|
151
164
|
emit({ type: "playing", ...song });
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
// A listener may stop the queue as the song starts, as Moondog's stream does past its time limit.
|
|
166
|
+
if (controller.signal.aborted) break;
|
|
167
|
+
const how = await playFile(file);
|
|
168
|
+
// A made track that played to its end, that /skip ended, or that could not play, as the loop
|
|
169
|
+
// back from listening and Moondog's stream count it; /stop aborts the queue first.
|
|
170
|
+
if (generated && !controller.signal.aborted) emit({ type: { ended: "finished", stopped: "skipped", failed: "failed" }[how], ...song });
|
|
171
|
+
if (!tracks[index].file) await rm(file, { force: true });
|
|
154
172
|
if (controller.signal.aborted) break;
|
|
155
173
|
}
|
|
156
174
|
} finally {
|
|
@@ -197,6 +215,39 @@ export async function createTerminalPreviewPlayer({
|
|
|
197
215
|
});
|
|
198
216
|
start(accepted);
|
|
199
217
|
},
|
|
218
|
+
// A playlist from the listener's library: Apple Music previews and tracks Moondog made, in one queue.
|
|
219
|
+
// A song is { title, artist_credit, catalog_url, preview_url }; a made track { generated: true, id, title, file }.
|
|
220
|
+
playList(tracks) {
|
|
221
|
+
if (closed) return;
|
|
222
|
+
const accepted = (Array.isArray(tracks) ? tracks : []).map((track) => (track?.generated === true
|
|
223
|
+
? { title: String(track.title ?? "").trim(), artist_credit: "", generated: true, id: String(track.id ?? ""),
|
|
224
|
+
file: typeof track.file === "string" && path.isAbsolute(track.file) ? track.file : null }
|
|
225
|
+
: { title: String(track?.title ?? "").trim(), artist_credit: String(track?.artist_credit ?? "").trim(),
|
|
226
|
+
catalog_url: https(track?.catalog_url, linkHost), preview: https(track?.preview_url, previewHost) }))
|
|
227
|
+
.filter((track) => track.title && (track.generated ? track.file : track.catalog_url && track.preview)).slice(0, longestPlaylist);
|
|
228
|
+
if (!accepted.length) return;
|
|
229
|
+
stop();
|
|
230
|
+
emit({ type: "queue", tracks: accepted.map(({ title, artist_credit, catalog_url, generated, id }) => (generated
|
|
231
|
+
? { title, artist_credit: "", generated: true, id } : { title, artist_credit, catalog_url })), album: null });
|
|
232
|
+
start(accepted);
|
|
233
|
+
},
|
|
234
|
+
// A track Moondog made on this Mac: { id, title, file }, played whole and kept after it ends.
|
|
235
|
+
playGenerated({ id, title, file } = {}) {
|
|
236
|
+
const name = String(title ?? "").trim();
|
|
237
|
+
if (closed || !name || typeof file !== "string" || !path.isAbsolute(file)) return;
|
|
238
|
+
stop();
|
|
239
|
+
const track = { title: name, artist_credit: "", generated: true, id: String(id ?? "") };
|
|
240
|
+
emit({ type: "queue", tracks: [{ title: track.title, artist_credit: "", generated: true, id: track.id }], album: null });
|
|
241
|
+
start([{ ...track, file }]);
|
|
242
|
+
},
|
|
243
|
+
// A made track queued behind the one playing, as Moondog's stream does; with nothing playing, it plays now.
|
|
244
|
+
enqueueGenerated({ id, title, file } = {}) {
|
|
245
|
+
const name = String(title ?? "").trim();
|
|
246
|
+
if (closed || !name || typeof file !== "string" || !path.isAbsolute(file)) return;
|
|
247
|
+
if (!queue || !queue.tracks.every((track) => track.generated)) return this.playGenerated({ id, title, file });
|
|
248
|
+
queue.tracks.push({ title: name, artist_credit: "", generated: true, id: String(id ?? ""), file });
|
|
249
|
+
now.appended({ title: name, generated: true, id: String(id ?? "") });
|
|
250
|
+
},
|
|
200
251
|
// Ends the song now playing, or still loading; the next one starts after it.
|
|
201
252
|
skip() {
|
|
202
253
|
if (!queue) return false;
|
|
@@ -205,6 +256,12 @@ export async function createTerminalPreviewPlayer({
|
|
|
205
256
|
return true;
|
|
206
257
|
},
|
|
207
258
|
stop,
|
|
259
|
+
// What plays now, as createPlayerNow reads it: null, or { state, song, position, count, next }.
|
|
260
|
+
nowPlaying: () => (closed ? null : now.read()),
|
|
261
|
+
// The song playing and every one after it, for keeping the whole queue in the library.
|
|
262
|
+
remaining: () => (closed ? [] : now.remaining()),
|
|
263
|
+
// Leaving Apple Music stops its previews here, never the tracks Moondog made.
|
|
264
|
+
holdsCatalogSongs: () => Boolean(queue?.tracks.some((track) => !track.generated)),
|
|
208
265
|
subscribe(listener) {
|
|
209
266
|
listeners.add(listener);
|
|
210
267
|
return () => listeners.delete(listener);
|
|
@@ -245,9 +245,10 @@ export function createNeteasePlayer({
|
|
|
245
245
|
* up. mode "play" replaces the queue and starts the first; "queue" adds after what is
|
|
246
246
|
* playing. Songs NetEase marks as not playable are never sent to the player, nor songs
|
|
247
247
|
* allow(song) refuses, such as the listener's Avoids; those are only counted. Before a lookup,
|
|
248
|
-
* allow gets the title and artist asked for, as { title, artist_credit }.
|
|
248
|
+
* allow gets the title and artist asked for, as { title, artist_credit }. beforePlay runs just
|
|
249
|
+
* before a song starts, which a queue with nothing loaded does too.
|
|
249
250
|
*/
|
|
250
|
-
async play({ items = [], mode = "play", intent, allow = () => true } = {}) {
|
|
251
|
+
async play({ items = [], mode = "play", intent, allow = () => true, beforePlay } = {}) {
|
|
251
252
|
const found = [];
|
|
252
253
|
const unplayable = [];
|
|
253
254
|
const notFound = [];
|
|
@@ -278,6 +279,7 @@ export function createNeteasePlayer({
|
|
|
278
279
|
queue = [...queue, ...songs];
|
|
279
280
|
outcome = "queued";
|
|
280
281
|
} else {
|
|
282
|
+
await beforePlay?.();
|
|
281
283
|
queue = songs;
|
|
282
284
|
await playAt(0);
|
|
283
285
|
outcome = "playing";
|
|
@@ -297,15 +299,19 @@ export function createNeteasePlayer({
|
|
|
297
299
|
checked(await run(["resume"]), "resume");
|
|
298
300
|
stayConnected();
|
|
299
301
|
},
|
|
300
|
-
|
|
302
|
+
// beforePlay runs just before a song starts, so the other players go quiet only for one.
|
|
303
|
+
async next({ beforePlay } = {}) {
|
|
301
304
|
if (index + 1 >= queue.length) throw failure("netease_failed", "Nothing is queued after this song.");
|
|
305
|
+
await beforePlay?.();
|
|
302
306
|
await playAt(index + 1);
|
|
303
307
|
},
|
|
304
308
|
// Back to the start of the song, or to the one before when it has only just begun.
|
|
305
|
-
async previous() {
|
|
309
|
+
async previous({ beforePlay } = {}) {
|
|
306
310
|
const now = await state();
|
|
307
|
-
if (index > 0 && (Number(now.position) || 0) <= 3)
|
|
308
|
-
|
|
311
|
+
if (index > 0 && (Number(now.position) || 0) <= 3) {
|
|
312
|
+
await beforePlay?.();
|
|
313
|
+
await playAt(index - 1);
|
|
314
|
+
} else checked(await run(["seek", "0"]), "go back to the start");
|
|
309
315
|
},
|
|
310
316
|
async seek({ positionMs = 0 } = {}) {
|
|
311
317
|
checked(await run(["seek", String(Math.max(0, Math.round(positionMs / 1000)))]), "move in the song");
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Playlist links the listener pastes into the library: Spotify, Apple Music, NetEase Music and QQ Music.
|
|
2
|
+
// Each link is read for its name and songs only; nothing signs in to a service it doesn't already have,
|
|
3
|
+
// and nothing but the official hosts is ever fetched.
|
|
4
|
+
import { fetchPublicPlaylist } from "./public-playlists.mjs";
|
|
5
|
+
|
|
6
|
+
const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
7
|
+
const PUBLIC_HOSTS = new Set(["y.qq.com", "c.y.qq.com", "i.y.qq.com", "m.y.qq.com", "music.163.com", "y.music.163.com", "163cn.tv"]);
|
|
8
|
+
const APPLE_PAGE_MAX = 12 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
/** The one playlist link in some text: { provider, id, url } for Spotify and Apple Music, or { provider: "public", url } for NetEase and QQ. */
|
|
11
|
+
export function parsePlaylistLink(text) {
|
|
12
|
+
const value = String(text ?? "").trim();
|
|
13
|
+
const uri = /^spotify:playlist:([A-Za-z0-9]{10,40})$/u.exec(value);
|
|
14
|
+
if (uri) return { provider: "spotify", id: uri[1], url: `https://open.spotify.com/playlist/${uri[1]}` };
|
|
15
|
+
const links = value.match(/https?:\/\/[^\s<>"“”]+/gu) ?? [];
|
|
16
|
+
if (links.length !== 1) fail("playlist_link_missing", "Paste one playlist link: Spotify, Apple Music, NetEase Music or QQ Music.");
|
|
17
|
+
let url;
|
|
18
|
+
try { url = new URL(links[0]); } catch { fail("playlist_link_missing", "That link can't be read."); }
|
|
19
|
+
if (url.username || url.password || url.port) fail("playlist_link_unsupported", "That link can't be read.");
|
|
20
|
+
if (url.hostname === "open.spotify.com") {
|
|
21
|
+
const match = /^\/(?:intl-[a-z-]+\/)?playlist\/([A-Za-z0-9]{10,40})\/?$/u.exec(url.pathname);
|
|
22
|
+
if (match) return { provider: "spotify", id: match[1], url: `https://open.spotify.com/playlist/${match[1]}` };
|
|
23
|
+
}
|
|
24
|
+
if (url.hostname === "music.apple.com" || url.hostname === "embed.music.apple.com") {
|
|
25
|
+
const match = /^\/([a-z]{2})\/playlist\/(?:[^/]+\/)?(pl\.[A-Za-z0-9.-]{6,80})\/?$/u.exec(url.pathname);
|
|
26
|
+
if (match) return { provider: "apple_music", id: match[2], storefront: match[1], url: `https://music.apple.com/${match[1]}/playlist/${match[2]}` };
|
|
27
|
+
}
|
|
28
|
+
if (PUBLIC_HOSTS.has(url.hostname)) return { provider: "public", url: url.href };
|
|
29
|
+
fail("playlist_link_unsupported", "Moondog reads playlist links from Spotify, Apple Music, NetEase Music and QQ Music.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The page's text, bounded, from Apple's own host only and never through a redirect elsewhere.
|
|
33
|
+
async function applePage(url, fetchImpl, signal) {
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
response = await fetchImpl(url, { redirect: "follow", signal: AbortSignal.any([AbortSignal.timeout(20_000), ...(signal ? [signal] : [])]),
|
|
37
|
+
headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Moondog playlist import)", "Accept-Language": "en" } });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (signal?.aborted) throw error;
|
|
40
|
+
fail("playlist_unreachable", "Couldn't reach Apple Music. Check your connection and try the link again.");
|
|
41
|
+
}
|
|
42
|
+
if (response.url && new URL(response.url).hostname !== "music.apple.com") fail("playlist_unavailable", "Apple Music answered from another address.");
|
|
43
|
+
if (!response.ok) { await response.body?.cancel(); fail("playlist_unavailable", "Apple Music didn't show this playlist. Check that it opens in a browser without signing in."); }
|
|
44
|
+
const chunks = [];
|
|
45
|
+
let size = 0;
|
|
46
|
+
for await (const chunk of response.body) {
|
|
47
|
+
size += chunk.length;
|
|
48
|
+
if (size > APPLE_PAGE_MAX) fail("playlist_too_large", "This playlist page is too large to read.");
|
|
49
|
+
chunks.push(chunk);
|
|
50
|
+
}
|
|
51
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const text = (value, maximum = 200) => (typeof value === "string" ? Array.from(value.replace(/\s+/gu, " ").trim()).slice(0, maximum).join("") : "");
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An Apple Music playlist from its public page: its name, how many songs it has, and the songs the page
|
|
58
|
+
* lists with their Apple Music ids and covers. A long playlist's page may list only its first songs;
|
|
59
|
+
* read and total say how many.
|
|
60
|
+
*/
|
|
61
|
+
export async function readApplePlaylist({ id, url }, { fetchImpl = fetch, signal } = {}) {
|
|
62
|
+
const html = await applePage(url, fetchImpl, signal);
|
|
63
|
+
const block = (pattern) => { const match = pattern.exec(html); try { return match ? JSON.parse(match[1]) : null; } catch { return null; } };
|
|
64
|
+
const schema = block(/<script[^>]*id=schema:music-playlist[^>]*>([\s\S]*?)<\/script>/u) ?? block(/<script[^>]*application\/ld\+json[^>]*>([\s\S]*?)<\/script>/u);
|
|
65
|
+
const data = block(/<script[^>]*id="serialized-server-data"[^>]*>([\s\S]*?)<\/script>/u);
|
|
66
|
+
const tracks = [];
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
// The server data lists each song as a track lockup with its title, its artist and its catalog id.
|
|
69
|
+
const walk = (node, depth) => {
|
|
70
|
+
if (!node || typeof node !== "object" || depth > 12 || tracks.length >= 2000) return;
|
|
71
|
+
if (Array.isArray(node)) { for (const each of node) walk(each, depth + 1); return; }
|
|
72
|
+
const catalog = node.contentDescriptor?.identifiers?.storeAdamID;
|
|
73
|
+
if (typeof node.title === "string" && typeof node.artistName === "string" && node.contentDescriptor?.kind === "song" && catalog) {
|
|
74
|
+
const key = String(catalog);
|
|
75
|
+
if (!seen.has(key)) {
|
|
76
|
+
seen.add(key);
|
|
77
|
+
const artwork = typeof node.artwork?.dictionary?.url === "string" ? node.artwork.dictionary.url.replace("{w}x{h}", "300x300").replace("{f}", "jpg") : null;
|
|
78
|
+
tracks.push({ id: key, title: text(node.title), artist: text(node.artistName), ...(artwork?.startsWith("https://") ? { artwork } : {}) });
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const value of Object.values(node)) walk(value, depth + 1);
|
|
83
|
+
};
|
|
84
|
+
walk(data, 0);
|
|
85
|
+
const name = text(schema?.name) || text(/<meta property="og:title" content="([^"]+)"/u.exec(html)?.[1]) || "Apple Music playlist";
|
|
86
|
+
const total = Number.isSafeInteger(Number(schema?.numTracks)) ? Number(schema.numTracks) : null;
|
|
87
|
+
if (!tracks.length) fail("playlist_unavailable", "Apple Music didn't list the songs of this playlist. Check that it is shared publicly.");
|
|
88
|
+
return { provider: "apple_music", id, url, name, total: total !== null && total >= tracks.length ? total : null, tracks: tracks.filter((track) => track.title && track.artist) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A NetEase Music or QQ Music playlist through its public share link, as library songs. */
|
|
92
|
+
export async function readPublicPlaylistLink({ url }, { fetchImpl = fetch } = {}) {
|
|
93
|
+
const playlist = await fetchPublicPlaylist({ input: url, fetchImpl });
|
|
94
|
+
return { provider: playlist.provider, id: playlist.id, url: playlist.url, name: playlist.name, total: playlist.total,
|
|
95
|
+
tracks: playlist.rows.map((row) => ({ id: row.id, title: row.title, artist: row.artist, ...(row.album ? { album: row.album } : {}) })).filter((track) => track.artist) };
|
|
96
|
+
}
|