init repo
This commit is contained in:
commit
e8314a95bf
|
@ -0,0 +1,152 @@
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/go,goland
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=go,goland
|
||||||
|
|
||||||
|
### Go ###
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# config files
|
||||||
|
config.yaml
|
||||||
|
|
||||||
|
# log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# build files
|
||||||
|
build/*
|
||||||
|
|
||||||
|
# database files
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
### GoLand ###
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### GoLand Patch ###
|
||||||
|
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||||
|
|
||||||
|
# *.iml
|
||||||
|
# modules.xml
|
||||||
|
# .idea/misc.xml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# Sonarlint plugin
|
||||||
|
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||||
|
.idea/**/sonarlint/
|
||||||
|
|
||||||
|
# SonarQube Plugin
|
||||||
|
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||||
|
.idea/**/sonarIssues.xml
|
||||||
|
|
||||||
|
# Markdown Navigator plugin
|
||||||
|
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||||
|
.idea/**/markdown-navigator.xml
|
||||||
|
.idea/**/markdown-navigator-enh.xml
|
||||||
|
.idea/**/markdown-navigator/
|
||||||
|
|
||||||
|
# Cache file creation bug
|
||||||
|
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||||
|
.idea/$CACHE_FILE$
|
||||||
|
|
||||||
|
# CodeStream plugin
|
||||||
|
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||||
|
.idea/codestream.xml
|
||||||
|
|
||||||
|
# Azure Toolkit for IntelliJ plugin
|
||||||
|
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
|
||||||
|
.idea/**/azureSettings.xml
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/go,goland
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
|
@ -0,0 +1,10 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="GoDfaErrorMayBeNotNil" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<functions>
|
||||||
|
<function importPath="ncmdump/ncmcrypt" name="NewNeteaseCloudMusic" />
|
||||||
|
</functions>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MaterialThemeProjectNewConfig">
|
||||||
|
<option name="metadata">
|
||||||
|
<MTProjectMetadataState>
|
||||||
|
<option name="migrated" value="true" />
|
||||||
|
<option name="pristineConfig" value="false" />
|
||||||
|
<option name="userId" value="-621f8711:18bafb1603a:-8000" />
|
||||||
|
<option name="version" value="8.13.2" />
|
||||||
|
</MTProjectMetadataState>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/ncmdump-go.iml" filepath="$PROJECT_DIR$/.idea/ncmdump-go.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,17 @@
|
||||||
|
module ncmdump
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require github.com/tidwall/gjson v1.17.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bogem/id3v2/v2 v2.1.4 // indirect
|
||||||
|
github.com/frolovo22/tag v0.0.2 // indirect
|
||||||
|
github.com/go-flac/flacpicture v0.3.0 // indirect
|
||||||
|
github.com/go-flac/go-flac v1.0.0 // indirect
|
||||||
|
github.com/go-flac/go-flac/v2 v2.0.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
golang.org/x/text v0.18.0 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,45 @@
|
||||||
|
github.com/bogem/id3v2/v2 v2.1.4 h1:CEwe+lS2p6dd9UZRlPc1zbFNIha2mb2qzT1cCEoNWoI=
|
||||||
|
github.com/bogem/id3v2/v2 v2.1.4/go.mod h1:l+gR8MZ6rc9ryPTPkX77smS5Me/36gxkMgDayZ9G1vY=
|
||||||
|
github.com/frolovo22/tag v0.0.2 h1:gFv5P5nqE7purEipbKT7X/OjP286nx5gA30mjt/4SgA=
|
||||||
|
github.com/frolovo22/tag v0.0.2/go.mod h1:Bt1H06v6RQFTrplGixhtUXVzHA/RpmhGEVxC7wqWGIw=
|
||||||
|
github.com/go-flac/flacpicture v0.3.0 h1:LkmTxzFLIynwfhHiZsX0s8xcr3/u33MzvV89u+zOT8I=
|
||||||
|
github.com/go-flac/flacpicture v0.3.0/go.mod h1:DPbrzVYQ3fJcvSgLFp9HXIrEQEdfdk/+m0nQCzwodZI=
|
||||||
|
github.com/go-flac/go-flac v1.0.0 h1:6qI9XOVLcO50xpzm3nXvO31BgDgHhnr/p/rER/K/doY=
|
||||||
|
github.com/go-flac/go-flac v1.0.0/go.mod h1:WnZhcpmq4u1UdZMNn9LYSoASpWOCMOoxXxcWEHSzkW8=
|
||||||
|
github.com/go-flac/go-flac/v2 v2.0.1 h1:1zilNkbmmpK9DLsz2NbjLHG8avOmthYqUfVc9YKB/Ps=
|
||||||
|
github.com/go-flac/go-flac/v2 v2.0.1/go.mod h1:hvgeR2hElLbwk0Q1/vMazIDmIc2LAFSd9Bx/Fk6ViKo=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||||
|
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
@ -0,0 +1,104 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
"ncmdump/ncmcrypt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func processFile(filePath string) error {
|
||||||
|
currentFile, err := ncmcrypt.NewNeteaseCloudMusic(filePath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[Error] Reading '%s' failed: '%s'\n", filePath, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dump, err := currentFile.Dump()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[Error] Processing '%s' failed: '%s'\n", filePath, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if dump {
|
||||||
|
metadata, _ := currentFile.FixMetadata()
|
||||||
|
if !metadata {
|
||||||
|
fmt.Fprintf(os.Stderr, "[Warning] Fix metadata for '%s' failed: '%s'\n", filePath, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("[Done] '%s' -> '%s'\n", filePath, currentFile.GetDumpFilePath())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var folderPath string
|
||||||
|
showHelp := flag.BoolP("help", "h", false, "Display help message")
|
||||||
|
showVersion := flag.BoolP("version", "v", false, "Display version information")
|
||||||
|
flag.StringVarP(&folderPath, "dir", "d", "", "Process all files in the directory")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if len(os.Args) == 1 {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *showHelp {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *showVersion {
|
||||||
|
fmt.Println("ncmdump version 1.5.0")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if folderPath != "" {
|
||||||
|
// check if the folder exists
|
||||||
|
info, err := os.Stat(folderPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[Error] Unable to access directory '%s'", folderPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
fmt.Fprintf(os.Stderr, "[Error] '%s' is not a directory", folderPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dump files in the folder
|
||||||
|
files, err := os.ReadDir(folderPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[Error] Unable to read directory '%s'", folderPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(folderPath, file.Name())
|
||||||
|
// skip if the extension is not .ncm
|
||||||
|
if filePath[len(filePath)-4:] != ".ncm" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = processFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// dump file from args
|
||||||
|
for _, filePath := range flag.Args() {
|
||||||
|
// skip if the extension is not .ncm
|
||||||
|
if filePath[len(filePath)-4:] != ".ncm" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := processFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package ncmcrypt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NeteaseClousMusicMetadata struct {
|
||||||
|
mAlbum string
|
||||||
|
mArtist string
|
||||||
|
mFormat string
|
||||||
|
mName string
|
||||||
|
mDuration int64
|
||||||
|
mBitrate int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNeteaseCloudMusicMetadata(meta string) *NeteaseClousMusicMetadata {
|
||||||
|
if meta == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
metaData := &NeteaseClousMusicMetadata{
|
||||||
|
mAlbum: "",
|
||||||
|
mArtist: "",
|
||||||
|
mFormat: "",
|
||||||
|
mName: "",
|
||||||
|
mDuration: 0,
|
||||||
|
mBitrate: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
metaData.mName = gjson.Get(meta, "musicName").String()
|
||||||
|
metaData.mAlbum = gjson.Get(meta, "album").String()
|
||||||
|
|
||||||
|
artists := gjson.Get(meta, "artist").Array()
|
||||||
|
if len(artists) > 0 {
|
||||||
|
for i, artist := range artists {
|
||||||
|
if i > 0 {
|
||||||
|
metaData.mArtist += "/"
|
||||||
|
}
|
||||||
|
metaData.mArtist += artist.Array()[0].String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metaData.mBitrate = gjson.Get(meta, "bitrate").Int()
|
||||||
|
metaData.mDuration = gjson.Get(meta, "duration").Int()
|
||||||
|
metaData.mFormat = gjson.Get(meta, "format").String()
|
||||||
|
|
||||||
|
return metaData
|
||||||
|
}
|
|
@ -0,0 +1,323 @@
|
||||||
|
package ncmcrypt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"github.com/bogem/id3v2/v2"
|
||||||
|
"github.com/go-flac/flacpicture"
|
||||||
|
"github.com/go-flac/go-flac"
|
||||||
|
"ncmdump/utils"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NcmFormat = string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Mp3 NcmFormat = "mp3"
|
||||||
|
)
|
||||||
|
const (
|
||||||
|
Flac NcmFormat = "flac"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NeteaseCloudMusic struct {
|
||||||
|
sCoreKey [17]byte
|
||||||
|
sModifyKey [17]byte
|
||||||
|
mPng [8]byte
|
||||||
|
|
||||||
|
mFilePath string
|
||||||
|
|
||||||
|
mDumpFilePath string
|
||||||
|
mFormat NcmFormat
|
||||||
|
mImageData []byte
|
||||||
|
mFileStream *os.File
|
||||||
|
mKeyBox [256]byte
|
||||||
|
mMetadata *NeteaseClousMusicMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ncm *NeteaseCloudMusic) read(buffer *[]byte, size int) int {
|
||||||
|
if len(*buffer) < size {
|
||||||
|
*buffer = make([]byte, size)
|
||||||
|
}
|
||||||
|
res, err := ncm.mFileStream.Read(*buffer)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ncm *NeteaseCloudMusic) openFile() bool {
|
||||||
|
file, err := os.Open(ncm.mFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ncm.mFileStream = file
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ncm *NeteaseCloudMusic) isNcmFile() bool {
|
||||||
|
header := make([]byte, 4)
|
||||||
|
|
||||||
|
// check magic header 4E455443 4D414446
|
||||||
|
if ncm.read(&header, 4) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if int(binary.LittleEndian.Uint32(header)) != 0x4E455443 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ncm.read(&header, 4) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(binary.LittleEndian.Uint32(header)) != 0x4D414446 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ncm *NeteaseCloudMusic) buildKeyBox(key []byte, keyLen int) {
|
||||||
|
for i := 0; i < 256; i++ {
|
||||||
|
ncm.mKeyBox[i] = byte(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
var swap uint8 = 0
|
||||||
|
var c uint8 = 0
|
||||||
|
var lastByte uint8 = 0
|
||||||
|
var keyOffset uint8 = 0
|
||||||
|
|
||||||
|
for i := 0; i < 256; i++ {
|
||||||
|
swap = ncm.mKeyBox[i]
|
||||||
|
c = (swap + lastByte + key[keyOffset]) & 0xff
|
||||||
|
keyOffset++
|
||||||
|
if int(keyOffset) >= keyLen {
|
||||||
|
keyOffset = 0
|
||||||
|
}
|
||||||
|
ncm.mKeyBox[i] = ncm.mKeyBox[c]
|
||||||
|
ncm.mKeyBox[c] = swap
|
||||||
|
lastByte = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ncm *NeteaseCloudMusic) mimeType() string {
|
||||||
|
if bytes.HasPrefix(ncm.mImageData, ncm.mPng[:]) {
|
||||||
|
return "image/png"
|
||||||
|
}
|
||||||
|
return "image/jpeg"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ncm *NeteaseCloudMusic) Dump() (bool, error) {
|
||||||
|
ncm.mDumpFilePath = ncm.mFilePath
|
||||||
|
var outputStream *os.File
|
||||||
|
|
||||||
|
buffer := make([]byte, 0x8000)
|
||||||
|
findFormatFlag := false
|
||||||
|
|
||||||
|
for {
|
||||||
|
n := ncm.read(&buffer, len(buffer))
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
j := (i + 1) & 0xff
|
||||||
|
buffer[i] ^= ncm.mKeyBox[(ncm.mKeyBox[j]+ncm.mKeyBox[(int(ncm.mKeyBox[j])+j)&0xff])&0xff]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !findFormatFlag {
|
||||||
|
if buffer[0] == 0x49 && buffer[1] == 0x44 && buffer[2] == 0x33 {
|
||||||
|
ncm.mFormat = Mp3
|
||||||
|
ncm.mDumpFilePath = utils.ReplaceExtension(ncm.mDumpFilePath, ".mp3")
|
||||||
|
} else {
|
||||||
|
ncm.mFormat = Flac
|
||||||
|
ncm.mDumpFilePath = utils.ReplaceExtension(ncm.mDumpFilePath, ".flac")
|
||||||
|
}
|
||||||
|
findFormatFlag = true
|
||||||
|
|
||||||
|
output, err := os.Create(ncm.mDumpFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("create output file failed")
|
||||||
|
}
|
||||||
|
outputStream = output
|
||||||
|
}
|
||||||
|
|
||||||
|
outputStream.Write(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputStream.Close()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ncm *NeteaseCloudMusic) FixMetadata() (bool, error) {
|
||||||
|
if ncm.mFormat == Mp3 {
|
||||||
|
audioFile, err := id3v2.Open(ncm.mDumpFilePath, id3v2.Options{Parse: true})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer audioFile.Close()
|
||||||
|
audioFile.SetTitle(ncm.mMetadata.mName)
|
||||||
|
audioFile.SetArtist(ncm.mMetadata.mArtist)
|
||||||
|
audioFile.SetAlbum(ncm.mMetadata.mAlbum)
|
||||||
|
|
||||||
|
if len(ncm.mImageData) > 0 {
|
||||||
|
pic := id3v2.PictureFrame{
|
||||||
|
Encoding: id3v2.EncodingUTF8,
|
||||||
|
MimeType: ncm.mimeType(),
|
||||||
|
PictureType: id3v2.PTFrontCover,
|
||||||
|
Description: "",
|
||||||
|
Picture: ncm.mImageData,
|
||||||
|
}
|
||||||
|
audioFile.AddAttachedPicture(pic)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = audioFile.Save()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
} else if ncm.mFormat == Flac {
|
||||||
|
audioFile, err := flac.ParseFile(ncm.mDumpFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if len(ncm.mImageData) > 0 {
|
||||||
|
pic, err := flacpicture.NewFromImageData(flacpicture.PictureTypeFrontCover, "",
|
||||||
|
ncm.mImageData, ncm.mimeType())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
pictureMeta := pic.Marshal()
|
||||||
|
audioFile.Meta = append(audioFile.Meta, &pictureMeta)
|
||||||
|
}
|
||||||
|
generalMeta := &flac.MetaDataBlock{
|
||||||
|
Type: flac.VorbisComment,
|
||||||
|
Data: []byte(fmt.Sprintf("title=%s\nartist=%s\nalbum=%s", ncm.mMetadata.mName, ncm.mMetadata.mArtist, ncm.mMetadata.mAlbum)),
|
||||||
|
}
|
||||||
|
audioFile.Meta = append(audioFile.Meta, generalMeta)
|
||||||
|
err = audioFile.Save(ncm.mDumpFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ncm *NeteaseCloudMusic) GetDumpFilePath() string {
|
||||||
|
return ncm.mDumpFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNeteaseCloudMusic(filePath string) (*NeteaseCloudMusic, error) {
|
||||||
|
ncm := &NeteaseCloudMusic{
|
||||||
|
sCoreKey: [17]byte{0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57, 0},
|
||||||
|
sModifyKey: [17]byte{0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28, 0},
|
||||||
|
mPng: [8]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A},
|
||||||
|
|
||||||
|
mFilePath: filePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ncm.openFile() {
|
||||||
|
return nil, fmt.Errorf("open file failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ncm.isNcmFile() {
|
||||||
|
return nil, fmt.Errorf("not a ncm file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// actually this 2 bytes is the version, now we just skip it
|
||||||
|
if _, err := ncm.mFileStream.Seek(2, 1); err != nil {
|
||||||
|
return nil, fmt.Errorf("seek version failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// the length of the RC4 key, encrypted by AES128
|
||||||
|
n := make([]byte, 4)
|
||||||
|
|
||||||
|
if ncm.read(&n, len(n)) != 4 {
|
||||||
|
return nil, fmt.Errorf("read key len failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
keyLen := int(binary.LittleEndian.Uint32(n))
|
||||||
|
|
||||||
|
keydata := make([]byte, keyLen)
|
||||||
|
ncm.read(&keydata, keyLen)
|
||||||
|
|
||||||
|
for i := range keydata {
|
||||||
|
keydata[i] ^= 0x64
|
||||||
|
}
|
||||||
|
|
||||||
|
mKeyData, err := utils.AesEcbDecrypt(ncm.sCoreKey[:16], keydata)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decrypt key failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the key box
|
||||||
|
ncm.buildKeyBox(mKeyData[17:], len(mKeyData)-17)
|
||||||
|
|
||||||
|
if ncm.read(&n, len(n)) != 4 {
|
||||||
|
return nil, fmt.Errorf("read metadata len failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataLen := int(binary.LittleEndian.Uint32(n))
|
||||||
|
|
||||||
|
if metadataLen <= 0 {
|
||||||
|
// process meta here
|
||||||
|
ncm.mMetadata = nil
|
||||||
|
} else {
|
||||||
|
// read metadata
|
||||||
|
modifyData := make([]byte, metadataLen)
|
||||||
|
ncm.read(&modifyData, metadataLen)
|
||||||
|
|
||||||
|
for i := range modifyData {
|
||||||
|
modifyData[i] ^= 0x63
|
||||||
|
}
|
||||||
|
|
||||||
|
// escape `163 key(Don't modify):`
|
||||||
|
swapModifyData := string(modifyData[22:])
|
||||||
|
|
||||||
|
modifyOutData, err := base64.StdEncoding.DecodeString(swapModifyData)
|
||||||
|
if err != nil {
|
||||||
|
panic("base64 decode modify data failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyDecryptData, err := utils.AesEcbDecrypt(ncm.sModifyKey[:16], modifyOutData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic("decrypt modify data failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// escape `music:`
|
||||||
|
mMetadataString := string(modifyDecryptData[6:])
|
||||||
|
|
||||||
|
ncm.mMetadata = NewNeteaseCloudMusicMetadata(mMetadataString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip the 5 bytes gap
|
||||||
|
if _, err := ncm.mFileStream.Seek(5, 1); err != nil {
|
||||||
|
return nil, fmt.Errorf("seek gap failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the cover frame
|
||||||
|
coverFrameLen := make([]byte, 4)
|
||||||
|
|
||||||
|
if ncm.read(&coverFrameLen, len(coverFrameLen)) != 4 {
|
||||||
|
return nil, fmt.Errorf("read cover frame len failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ncm.read(&n, len(n)) != 4 {
|
||||||
|
return nil, fmt.Errorf("read cover frame data len failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
coverFrameLenInt := int(binary.LittleEndian.Uint32(coverFrameLen))
|
||||||
|
coverFrameDataLen := int(binary.LittleEndian.Uint32(n))
|
||||||
|
|
||||||
|
if coverFrameDataLen > 0 {
|
||||||
|
ncm.read(&ncm.mImageData, coverFrameDataLen)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[Warning] Missing album: '%s'\n", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
ncm.mFileStream.Seek(int64(coverFrameLenInt)-int64(coverFrameDataLen), 1)
|
||||||
|
|
||||||
|
return ncm, nil
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AesEcbDecrypt(key []byte, src []byte) ([]byte, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := make([]byte, 0, len(src))
|
||||||
|
tmp := make([]byte, aes.BlockSize)
|
||||||
|
|
||||||
|
for i := 0; i < len(src); i += aes.BlockSize {
|
||||||
|
block.Decrypt(tmp, src[i:i+aes.BlockSize])
|
||||||
|
|
||||||
|
if i == len(src)-aes.BlockSize {
|
||||||
|
pad := int(tmp[len(tmp)-1])
|
||||||
|
if pad > aes.BlockSize {
|
||||||
|
pad = 0
|
||||||
|
}
|
||||||
|
dst = append(dst, tmp[:aes.BlockSize-pad]...)
|
||||||
|
} else {
|
||||||
|
dst = append(dst, tmp...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dst, nil
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReplaceExtension(filepathStr, newExt string) string {
|
||||||
|
ext := filepath.Ext(filepathStr)
|
||||||
|
return strings.TrimSuffix(filepathStr, ext) + newExt
|
||||||
|
}
|
Loading…
Reference in New Issue