├── .dockerignore ├── Dockerfile ├── versions_v2.3.json ├── .gitignore ├── docker-compose.yml ├── .github └── workflows │ ├── build.yml │ └── docker-build.yml ├── config-compose.ini ├── config_example.ini ├── versions.json ├── bot ├── db.go ├── processSearch.go ├── pic.go ├── init.go ├── musicRecognize.go ├── processLyric.go ├── modules.go ├── logger.go ├── tools.go ├── processInline.go ├── bot.go └── processMusic.go ├── go.mod ├── versions_v2.2.json ├── README.md ├── main.go ├── go.sum └── LICENSE /.dockerignore: -------------------------------------------------------------------------------- 1 | #GitHub Staff 2 | .github 3 | 4 | #Git 5 | .gitignore 6 | LICENSE 7 | README.md 8 | 9 | #Other 10 | config_example.ini 11 | 12 | #Docker 13 | docker-compose.yml -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang AS builder 2 | 3 | WORKDIR /build 4 | 5 | ADD . /build 6 | 7 | RUN bash build.sh 8 | 9 | FROM alpine:3.21 AS runner 10 | 11 | RUN apk add --no-cache ca-certificates 12 | 13 | WORKDIR /app 14 | 15 | COPY --from=builder /build/Music163bot-Go . 16 | 17 | CMD [ "./Music163bot-Go" ] 18 | -------------------------------------------------------------------------------- /versions_v2.3.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "v2.3.7", 4 | "version_code": 20307, 5 | "commit_sha": "6a28fef080d959500693737a6585a00da2e603d5" 6 | }, 7 | { 8 | "version": "v2.3.4", 9 | "version_code": 20304, 10 | "commit_sha": "18656d4ffc2d4dae32017a21798a0478f8768748" 11 | }, 12 | { 13 | "version": "v2.3.0", 14 | "version_code": 20300, 15 | "commit_sha": "ed1caae4c8a3395989f3d6a0c13096af00027248" 16 | } 17 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.bak 8 | *.db 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | test 13 | music 14 | pic 15 | log 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | /.idea/ 23 | config.ini 24 | /Music163bot-Go* 25 | /src 26 | /ext 27 | /cache 28 | /test 29 | *.mp3 30 | *.flac -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | bot-api: 3 | image: aiogram/telegram-bot-api:latest 4 | environment: 5 | TELEGRAM_API_ID: "" 6 | TELEGRAM_API_HASH: "" 7 | networks: 8 | - music163bot 9 | 10 | bot: 11 | image: ghcr.io/xiaomengxinx/music163bot-go 12 | volumes: 13 | - ./config.ini:/app/config.ini 14 | - ./cache:/app/cache 15 | - ./log:/app/log 16 | depends_on: 17 | bot-api: 18 | condition: service_started 19 | networks: 20 | - music163bot 21 | 22 | networks: 23 | music163bot: -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Debug build 2 | 3 | on: 4 | push: 5 | branches: 6 | - v2 7 | paths: 8 | - '**.go' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: '1.22' 21 | check-latest: true 22 | cache: true 23 | - name: Build 24 | run: | 25 | sudo timedatectl set-timezone Asia/Shanghai 26 | go mod tidy 27 | bash build.sh 28 | - name: Upload artifact 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: Music163bot-Go 32 | path: Music163bot-Go 33 | -------------------------------------------------------------------------------- /config-compose.ini: -------------------------------------------------------------------------------- 1 | # 以下为必填项 2 | # 你的 Bot Token 3 | BOT_TOKEN = YOUR_BOT_TOKEN 4 | 5 | # 你的网易云 cookie 中 MUSIC_U 项的值(用于下载无损歌曲) 6 | MUSIC_U = YOUR_MUSIC_U 7 | 8 | 9 | # 以下为可选项 10 | # 自定义 telegram bot API 地址 11 | BotAPI = http://bot-api:8081 12 | 13 | # 设置 bot 管理员 ID, 用 “," 分隔 14 | BotAdmin = 115414,1919810 15 | 16 | # 是否开启 bot 的 debug 功能 17 | BotDebug = false 18 | 19 | # 自定义 sqlite3 数据库文件 (默认为 cache.db) 20 | Database = cache/cache.db 21 | 22 | # 设置日志等级 [panic|fatal|error|warn|info|debug|trace] (默认为 info) 23 | LogLevel = info 24 | 25 | # 是否开启自动更新 (默认开启), 若设置为 false 相当于 -no-update 参数 26 | AutoUpdate = true 27 | 28 | # 下载文件损坏是否自动重新下载 (默认为 true) 29 | AutoRetry = true 30 | 31 | # 最大自动重试次数 (默认为 3) 32 | MaxRetryTimes = 3 33 | 34 | # 下载超时时长 (单位秒, 默认为 60) 35 | DownloadTimeout = 60 36 | 37 | # 自定义下载反向代理 38 | #ReverseProxy = 114.5.1.4:8080 -------------------------------------------------------------------------------- /config_example.ini: -------------------------------------------------------------------------------- 1 | # 以下为必填项 2 | # 你的 Bot Token 3 | BOT_TOKEN = YOUR_BOT_TOKEN 4 | 5 | # 你的网易云 cookie 中 MUSIC_U 项的值(用于下载无损歌曲) 6 | MUSIC_U = YOUR_MUSIC_U 7 | 8 | 9 | # 以下为可选项 10 | # 自定义 telegram bot API 地址 11 | BotAPI = https://api.telegram.org 12 | 13 | # 设置 bot 管理员 ID, 用 “," 分隔 14 | BotAdmin = 115414,1919810 15 | 16 | # 是否开启 bot 的 debug 功能 17 | BotDebug = false 18 | 19 | # 自定义 sqlite3 数据库文件 (默认为 cache.db) 20 | Database = cache.db 21 | 22 | # 设置日志等级 [panic|fatal|error|warn|info|debug|trace] (默认为 info) 23 | LogLevel = info 24 | 25 | # 是否开启自动更新 (默认开启), 若设置为 false 相当于 -no-update 参数 26 | AutoUpdate = true 27 | 28 | # 下载文件损坏是否自动重新下载 (默认为 true) 29 | AutoRetry = true 30 | 31 | # 最大自动重试次数 (默认为 3) 32 | MaxRetryTimes = 3 33 | 34 | # 下载超时时长 (单位秒, 默认为 60) 35 | DownloadTimeout = 60 36 | 37 | # 自定义下载反向代理 38 | ReverseProxy = 114.5.1.4:8080 -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "v2.1.8", 4 | "version_code": 20108, 5 | "commit_sha": "e2f8e849b73e88ac45eda9edf9e682e4f46d3c57" 6 | }, 7 | { 8 | "version": "v2.1.7", 9 | "version_code": 20107, 10 | "commit_sha": "133a871d632d8b77f4f8c1cf316d6ce86cdeed01" 11 | }, 12 | { 13 | "version": "v2.1.5", 14 | "version_code": 20105, 15 | "commit_sha": "a256e827ffe50021d0d1e2e91739fd848cabaded" 16 | }, 17 | { 18 | "version": "v2.1.3", 19 | "version_code": 20103, 20 | "commit_sha": "86d0900460fe3b2861ae7792bc7865fa1a3de65b" 21 | }, 22 | { 23 | "version": "v2.1.2", 24 | "version_code": 20102, 25 | "commit_sha": "447ea75710e0c5590e22198c515378edab090b9c" 26 | }, 27 | { 28 | "version": "v2.1.1", 29 | "version_code": 20101, 30 | "commit_sha": "9067e68d532184c25e2ecb02d66663066791065b" 31 | }, 32 | { 33 | "version": "v2.1.0", 34 | "version_code": 20100, 35 | "commit_sha": "95bb8b098f9a768063f6945fa3cec3b377f4eda7" 36 | }, 37 | { 38 | "version": "v2.0.0", 39 | "version_code": 20000, 40 | "commit_sha": "4a838f314d339e4c17544af862c0300973207075" 41 | } 42 | ] -------------------------------------------------------------------------------- /bot/db.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | "github.com/glebarez/sqlite" 6 | "gorm.io/gorm" 7 | "gorm.io/gorm/logger" 8 | ) 9 | 10 | // SongInfo 歌曲信息 11 | type SongInfo struct { 12 | gorm.Model 13 | MusicID int 14 | SongName string 15 | SongArtists string 16 | SongAlbum string 17 | FileExt string 18 | MusicSize int 19 | PicSize int 20 | EmbPicSize int 21 | BitRate int 22 | Duration int 23 | FileID string 24 | ThumbFileID string 25 | FromUserID int64 26 | FromUserName string 27 | FromChatID int64 28 | FromChatName string 29 | } 30 | 31 | func initDB(config map[string]string) (err error) { 32 | database := "cache.db" 33 | if config["Database"] != "" { 34 | database = config["Database"] 35 | } 36 | db, err := gorm.Open(sqlite.Open(fmt.Sprintf("%s?_pragma=busy_timeout(5000)", database)), &gorm.Config{ 37 | Logger: NewLogger(logger.Silent), 38 | PrepareStmt: true, 39 | }) 40 | if err != nil { 41 | return err 42 | } 43 | err = db.Table("song_infos").AutoMigrate(&SongInfo{}) 44 | if err != nil { 45 | return err 46 | } 47 | MusicDB = db.Table("song_infos") 48 | return err 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/XiaoMengXinX/Music163bot-Go/v2 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.1 6 | 7 | require ( 8 | github.com/XiaoMengXinX/163KeyMarker v0.0.0-20221030134715-67afb724a936 9 | github.com/XiaoMengXinX/Music163Api-Go v0.1.30 10 | github.com/XiaoMengXinX/SimpleDownloader v0.0.0-20241104184306-5642193c58ed 11 | github.com/glebarez/sqlite v1.11.0 12 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 13 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 14 | github.com/sirupsen/logrus v1.9.3 15 | gorm.io/gorm v1.25.12 16 | ) 17 | 18 | require ( 19 | github.com/bogem/id3v2 v1.2.0 // indirect 20 | github.com/dustin/go-humanize v1.0.1 // indirect 21 | github.com/glebarez/go-sqlite v1.22.0 // indirect 22 | github.com/go-flac/flacpicture v0.3.0 // indirect 23 | github.com/go-flac/flacvorbis v0.2.0 // indirect 24 | github.com/go-flac/go-flac v1.0.0 // indirect 25 | github.com/google/uuid v1.6.0 // indirect 26 | github.com/jinzhu/inflection v1.0.0 // indirect 27 | github.com/jinzhu/now v1.1.5 // indirect 28 | github.com/mattn/go-isatty v0.0.20 // indirect 29 | github.com/ncruces/go-strftime v0.1.9 // indirect 30 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 31 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 32 | golang.org/x/sys v0.26.0 // indirect 33 | golang.org/x/text v0.19.0 // indirect 34 | modernc.org/libc v1.61.0 // indirect 35 | modernc.org/mathutil v1.6.0 // indirect 36 | modernc.org/memory v1.8.0 // indirect 37 | modernc.org/sqlite v1.33.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | id-token: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v3 26 | 27 | - name: Log into registry ${{ env.REGISTRY }} 28 | if: github.event_name != 'pull_request' 29 | uses: docker/login-action@v3 30 | with: 31 | registry: ${{ env.REGISTRY }} 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Extract Docker metadata 36 | id: meta 37 | uses: docker/metadata-action@v5 38 | with: 39 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 40 | tags: | 41 | type=raw,value=latest 42 | type=ref,event=tag 43 | type=sha 44 | - name: Build and push Docker image 45 | id: build-and-push 46 | uses: docker/build-push-action@v6 47 | with: 48 | context: . 49 | push: ${{ github.event_name != 'pull_request' }} 50 | tags: ${{ steps.meta.outputs.tags }} 51 | labels: ${{ steps.meta.outputs.labels }} 52 | cache-from: type=gha 53 | cache-to: type=gha,mode=max -------------------------------------------------------------------------------- /bot/processSearch.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/XiaoMengXinX/Music163Api-Go/api" 7 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 8 | ) 9 | 10 | func processSearch(message tgbotapi.Message, bot *tgbotapi.BotAPI) (err error) { 11 | var msgResult tgbotapi.Message 12 | if message.CommandArguments() == "" { 13 | msg := tgbotapi.NewMessage(message.Chat.ID, inputKeyword) 14 | msg.ReplyToMessageID = message.MessageID 15 | msgResult, err = bot.Send(msg) 16 | return err 17 | } 18 | msg := tgbotapi.NewMessage(message.Chat.ID, searching) 19 | msg.ReplyToMessageID = message.MessageID 20 | msgResult, err = bot.Send(msg) 21 | if err != nil { 22 | return err 23 | } 24 | searchResult, _ := api.SearchSong(data, api.SearchSongConfig{ 25 | Keyword: message.CommandArguments(), 26 | Limit: 10, 27 | }) 28 | if len(searchResult.Result.Songs) == 0 { 29 | newEditMsg := tgbotapi.NewEditMessageText(message.Chat.ID, msgResult.MessageID, noResults) 30 | msgResult, err = bot.Send(newEditMsg) 31 | return err 32 | } 33 | var inlineButton []tgbotapi.InlineKeyboardButton 34 | var textMessage string 35 | for i := 0; i < len(searchResult.Result.Songs) && i < 8; i++ { 36 | var songArtists string 37 | for i, artist := range searchResult.Result.Songs[i].Artists { 38 | if i == 0 { 39 | songArtists = artist.Name 40 | } else { 41 | songArtists = fmt.Sprintf("%s/%s", songArtists, artist.Name) 42 | } 43 | } 44 | inlineButton = append(inlineButton, tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%d", i+1), fmt.Sprintf("music %d", searchResult.Result.Songs[i].Id))) 45 | textMessage = fmt.Sprintf("%s%d.「%s」 - %s\n", textMessage, i+1, searchResult.Result.Songs[i].Name, songArtists) 46 | } 47 | var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(inlineButton) 48 | newEditMsg := tgbotapi.NewEditMessageText(message.Chat.ID, msgResult.MessageID, textMessage) 49 | newEditMsg.ReplyMarkup = &numericKeyboard 50 | message, err = bot.Send(newEditMsg) 51 | if err != nil { 52 | return err 53 | } 54 | return err 55 | } 56 | -------------------------------------------------------------------------------- /bot/pic.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "image" 8 | "image/draw" 9 | "image/jpeg" 10 | "image/png" 11 | "io" 12 | "os" 13 | 14 | "github.com/nfnt/resize" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | // 缩放图片到 320x320px (黑底填充) 19 | func resizeImg(filePath string) (string, error) { 20 | file, err := os.Open(filePath) 21 | if err != nil { 22 | return "", err 23 | } 24 | 25 | buffer, err := io.ReadAll(bufio.NewReader(file)) 26 | if err != nil { 27 | return "", err 28 | } 29 | 30 | var img image.Image 31 | 32 | img, err = jpeg.Decode(bytes.NewReader(buffer)) 33 | if err != nil { 34 | img, err = png.Decode(bytes.NewReader(buffer)) 35 | if err != nil { 36 | return "", fmt.Errorf("Image decode error %s", filePath) 37 | } 38 | } 39 | err = file.Close() 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | width := img.Bounds().Dx() 45 | height := img.Bounds().Dy() 46 | widthNew := 320 47 | heightNew := 320 48 | 49 | var m image.Image 50 | if width/height >= widthNew/heightNew { 51 | m = resize.Resize(uint(widthNew), uint(height)*uint(widthNew)/uint(width), img, resize.Lanczos3) 52 | } else { 53 | m = resize.Resize(uint(width*heightNew/height), uint(heightNew), img, resize.Lanczos3) 54 | } 55 | 56 | newImag := image.NewNRGBA(image.Rect(0, 0, 320, 320)) 57 | if m.Bounds().Dx() > m.Bounds().Dy() { 58 | draw.Draw(newImag, image.Rectangle{ 59 | Min: image.Point{Y: (320 - m.Bounds().Dy()) / 2}, 60 | Max: image.Point{X: 320, Y: 320}, 61 | }, m, m.Bounds().Min, draw.Src) 62 | } else { 63 | draw.Draw(newImag, image.Rectangle{ 64 | Min: image.Point{X: (320 - m.Bounds().Dx()) / 2}, 65 | Max: image.Point{X: 320, Y: 320}, 66 | }, m, m.Bounds().Min, draw.Src) 67 | } 68 | 69 | out, err := os.Create(filePath + ".resize.jpg") 70 | if err != nil { 71 | return "", fmt.Errorf("Create image file error %s", err) 72 | } 73 | defer func(out *os.File) { 74 | err := out.Close() 75 | if err != nil { 76 | logrus.Errorln(err) 77 | } 78 | }(out) 79 | 80 | err = jpeg.Encode(out, newImag, nil) 81 | if err != nil { 82 | logrus.Fatal(err) 83 | } 84 | return filePath + ".resize.jpg", nil 85 | } 86 | -------------------------------------------------------------------------------- /versions_v2.2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "v2.2.18", 4 | "version_code": 20218, 5 | "commit_sha": "c49325710562b60aa695fa5a524c9532dac44f38" 6 | }, 7 | { 8 | "version": "v2.2.17", 9 | "version_code": 20217, 10 | "commit_sha": "3224e48da54369074817f044f2f8e5f30335cd18" 11 | }, 12 | { 13 | "version": "v2.2.16", 14 | "version_code": 20216, 15 | "commit_sha": "f3f8b09ab0097a219e8807d6717a8cafedaf12c1" 16 | }, 17 | { 18 | "version": "v2.2.15", 19 | "version_code": 20215, 20 | "commit_sha": "3e3317b09328916968ba3fe928d8db0147715a3d" 21 | }, 22 | { 23 | "version": "v2.2.14", 24 | "version_code": 20214, 25 | "commit_sha": "a8c4cb8080086d1481ca1baadb2bc4e18f755ddf" 26 | }, 27 | { 28 | "version": "v2.2.13", 29 | "version_code": 20213, 30 | "commit_sha": "69fcdf35114f3baf2f6d5194da7912b8739f27c5" 31 | }, 32 | { 33 | "version": "v2.2.12", 34 | "version_code": 20212, 35 | "commit_sha": "62ea4def344612f4a308d057cdbf541068634406" 36 | }, 37 | { 38 | "version": "v2.2.11", 39 | "version_code": 20211, 40 | "commit_sha": "fec24b3b0f4e775dcc98ad4a2efe5ed95f1ee8d3" 41 | }, 42 | { 43 | "version": "v2.2.10", 44 | "version_code": 20210, 45 | "commit_sha": "d78f4cc3e7630502afb16d2839a72db9a7018567" 46 | }, 47 | { 48 | "version": "v2.2.9", 49 | "version_code": 20209, 50 | "commit_sha": "2ca28f7f9bbe2c64d3565f7e56994e070e2d4d1b" 51 | }, 52 | { 53 | "version": "v2.2.8", 54 | "version_code": 20208, 55 | "commit_sha": "a7f4441ca1f7bf60b5d7b60fa7dae26082343267" 56 | }, 57 | { 58 | "version": "v2.2.7", 59 | "version_code": 20207, 60 | "commit_sha": "573092e8ddc490fdfad2cc8fa866a5789a2bb167" 61 | }, 62 | { 63 | "version": "v2.2.6", 64 | "version_code": 20206, 65 | "commit_sha": "e4d20f6a3bf7bac78094fc0e22ffeb2824d696cf" 66 | }, 67 | { 68 | "version": "v2.2.5", 69 | "version_code": 20205, 70 | "commit_sha": "ab98d8e1e7de7c3c6d462686bbbfa4c614db481e" 71 | }, 72 | { 73 | "version": "v2.2.4", 74 | "version_code": 20204, 75 | "commit_sha": "1af27281b0e71907d9618cd007289ed7a0af01f1" 76 | }, 77 | { 78 | "version": "v2.2.3", 79 | "version_code": 20203, 80 | "commit_sha": "7f3549ef5a2767cbf4e88c08891ad23ecfbfd5c8" 81 | }, 82 | { 83 | "version": "v2.2.2", 84 | "version_code": 20202, 85 | "commit_sha": "e706c4375c5a588a07cc469c976596665e3a0f98" 86 | }, 87 | { 88 | "version": "v2.2.1", 89 | "version_code": 20201, 90 | "commit_sha": "edb619eabb760c5458d7b37fd062ad20d626d267" 91 | }, 92 | { 93 | "version": "v2.2.0", 94 | "version_code": 20200, 95 | "commit_sha": "0a4710ff8806117091cc46f458e3f12b6e89f258" 96 | } 97 | ] -------------------------------------------------------------------------------- /bot/init.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/XiaoMengXinX/Music163Api-Go/utils" 8 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | // MusicDB 音乐缓存数据库入口 13 | var MusicDB *gorm.DB 14 | 15 | // config 配置文件数据 16 | var config map[string]string 17 | 18 | // data 网易云 cookie 19 | var data utils.RequestData 20 | 21 | var bot *tgbotapi.BotAPI 22 | var botAdmin []int 23 | var botAdminStr []string 24 | var botName string 25 | var cacheDir = "./cache" 26 | var botAPI = "https://api.telegram.org" 27 | 28 | // maxRetryTimes 最大重试次数, downloaderTimeout 下载超时时间 29 | var maxRetryTimes, downloaderTimeout int 30 | 31 | var ( 32 | reg1 = regexp.MustCompile(`(.*)song\?id=`) 33 | reg2 = regexp.MustCompile("(.*)song/") 34 | regP1 = regexp.MustCompile(`(.*)program\?id=`) 35 | regP2 = regexp.MustCompile("(.*)program/") 36 | regP3 = regexp.MustCompile(`(.*)dj\?id=`) 37 | regP4 = regexp.MustCompile("(.*)dj/") 38 | reg5 = regexp.MustCompile("/(.*)") 39 | reg4 = regexp.MustCompile("&(.*)") 40 | reg3 = regexp.MustCompile(`\?(.*)`) 41 | regInt = regexp.MustCompile(`\d+`) 42 | regUrl = regexp.MustCompile("(http|https)://[\\w\\-_]+(\\.[\\w\\-_]+)+([\\w\\-.,@?^=%&:/~+#]*[\\w\\-@?^=%&/~+#])?") 43 | ) 44 | 45 | var mdV2Replacer = strings.NewReplacer( 46 | "_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(", 47 | "\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>", 48 | "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|", 49 | "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!", 50 | ) 51 | 52 | var ( 53 | aboutText = `*Music163bot-Go v2* 54 | Github: https://github.com/XiaoMengXinX/Music163bot-Go 55 | 56 | \[编译环境] %s 57 | \[编译版本] %s 58 | \[编译哈希] %s 59 | \[编译日期] %s 60 | \[运行环境] %s` 61 | musicInfo = `「%s」- %s 62 | 专辑: %s 63 | #网易云音乐 #%s %.2fMB %.2fkbps 64 | via @%s` 65 | musicInfoMsg = `%s 66 | 专辑: %s 67 | %s %.2fMB 68 | ` 69 | uploadFailed = `下载/发送失败 70 | %v` 71 | statusInfo = `*\[统计信息\]* 72 | 数据库中总缓存歌曲数量: %d 73 | 当前对话 \[%s\] 缓存歌曲数量: %d 74 | 当前用户 \[[%d](tg://user?id=%d)\] 缓存歌曲数量: %d 75 | ` 76 | rmcacheReport = `清除 [%s] 缓存成功` 77 | inputKeyword = "请输入搜索关键词" 78 | inputIDorKeyword = "请输入歌曲ID或歌曲关键词" 79 | inputContent = "请输入歌曲关键词/歌曲分享链接/歌曲ID" 80 | searching = `搜索中...` 81 | noResults = `未找到结果` 82 | noCache = `歌曲未缓存` 83 | tapToDownload = `点击上方按钮缓存歌曲` 84 | tapMeToDown = `点我缓存歌曲` 85 | hitCache = `命中缓存, 正在发送中...` 86 | sendMeTo = `Send me to...` 87 | getLrcFailed = `获取歌词失败, 歌曲可能不存在或为纯音乐` 88 | getUrlFailed = `获取歌曲下载链接失败` 89 | fetchInfo = `正在获取歌曲信息...` 90 | fetchInfoFailed = `获取歌曲信息失败` 91 | waitForDown = `等待下载中...` 92 | downloading = `下载中...` 93 | downloadStatus = " %s\n%.2fMB/%.2fMB %d%%" 94 | redownloading = `下载失败,尝试重新下载中...` 95 | uploading = `下载完成, 发送中...` 96 | md5VerFailed = "MD5校验失败" 97 | reTrying = "尝试重新下载中 (%d/%d)" 98 | retryLater = "请稍后重试" 99 | 100 | reloading = "重新加载中" 101 | callbackText = "Success" 102 | 103 | fetchingLyric = "正在获取歌词中" 104 | downloadTimeout = `下载超时` 105 | ) 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Music163bot

2 | 3 |

一个用来下载/分享/搜索网易云歌曲的telegram bot

4 | 5 |

演示bot:https://t.me/Music163bot

6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 |

15 | 16 | ## ✨ 特性 17 | 18 | - 分享链接嗅探 19 | - inlinebot 20 | - 歌曲搜索 21 | - 为歌曲文件添加163key 22 | - 歌曲快速分享 23 | - 下载无损flac音频 (需设置网易云VIP账号的MUSIC_U) 24 | - 动态更新(使用 [traefik/yaegi](https://github.com/traefik/yaegi) 作为动态扩展框架) 25 | 26 | ## ⚙️ 构建 27 | 28 | 构建前请确保拥有 `Go 1.17`或更高版本 29 | 30 | **克隆代码** 31 | 32 | ``` 33 | git clone https://github.com/XiaoMengXinX/Music163bot-Go 34 | ``` 35 | 36 | **使用脚本自动编译 ( 支持 windows 的 bash 环境,例如 git bash )** 37 | 38 | ``` 39 | cd Music163bot-Go 40 | bash build.sh 41 | 42 | # 也可以加入环境变量以交叉编译,如 43 | GOOS=windows GOARCH=amd64 bash build.sh 44 | ``` 45 | 46 | ## 🛠️ 部署 47 | 48 | **修改配置文件** 49 | 50 | 打开项目根目录下的 `config_example.ini` 51 | 52 | ``` 53 | # 以下为必填项 54 | # 你的 Bot Token 55 | BOT_TOKEN = YOUR_BOT_TOKEN 56 | 57 | # 你的网易云 cookie 中 MUSIC_U 项的值(用于下载无损歌曲) 58 | MUSIC_U = YOUR_MUSIC_U 59 | 60 | 61 | # 以下为可选项 62 | # 自定义 telegram bot API 地址 63 | BotAPI = https://api.telegram.org 64 | 65 | # 设置 bot 管理员 ID, 用 “," 分隔 66 | BotAdmin = 1234,3456 67 | 68 | # 是否开启 bot 的 debug 功能 69 | BotDebug = false 70 | 71 | # 自定义 sqlite3 数据库文件 (默认为 cache.db) 72 | Database = cache.db 73 | 74 | # 设置日志等级 [panic|fatal|error|warn|info|debug|trace] (默认为 info) 75 | LogLevel = info 76 | 77 | # 是否开启自动更新 (默认开启), 若设置为 false 相当于 -no-update 参数 78 | AutoUpdate = true 79 | 80 | # 下载文件损坏是否自动重新下载 (默认为 true) 81 | AutoRetry = true 82 | 83 | # 最大自动重试次数 (默认为 3) 84 | MaxRetryTimes = 3 85 | 86 | # 下载超时时长 (单位秒, 默认为 60) 87 | DownloadTimeout = 60 88 | 89 | # 是否校验更新文件 md5 (默认开启), 若设置为 false 相当于 -no-md5-check 参数 90 | CheckMD5 = true 91 | 92 | # 自定义源码路径 93 | SrcPath = ./src 94 | 95 | # 自定义 bot 函数入口 (默认为 bot.Start) 96 | BotEntry = bot.Start 97 | ``` 98 | 99 | **※ 修改配置后,将 `config_example.ini` 重命名为 `config.ini`** 100 | 101 | **启动 Music163-bot** 102 | 103 | ``` 104 | $ ./Music163bot-Go 105 | 2021/10/30 13:05:40 [INFO] Music163bot-Go v2.0.0(20000) (main.go:122) 106 | 2021/10/30 13:05:40 [INFO] 正在检查更新中 (main.go:155) 107 | 2021/10/30 13:05:40 [INFO] v2.0.0(20000) 已是最新版本 (main.go:361) 108 | 2021/10/30 13:05:40 [INFO] 正在校验文件MD5 (main.go:164) 109 | 2021/10/30 13:05:40 [INFO] MD5校验成功 (main.go:169) 110 | 2021/10/30 13:05:40 [INFO] 加载版本 v2.0.0(20000) 中 (main.go:195) 111 | 2021/10/30 13:05:41 [INFO] Music163bot 验证成功 (value.go:543) 112 | ``` 113 | 114 | ## 🤖 命令 115 | 116 | - `/musicid` 或 `/netease` + `音乐ID` —— 从 MusicID 获取歌曲 117 | - `/search` + `关键词` —— 搜索歌曲 118 | - `/about` —— 关于本 bot 119 | -------------------------------------------------------------------------------- /bot/musicRecognize.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "mime/multipart" 9 | "net/http" 10 | "os" 11 | "os/exec" 12 | "time" 13 | 14 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 15 | ) 16 | 17 | var recognizeAPI = "https://music-recognize.vercel.app/api/recognize" 18 | 19 | func recognizeMusic(message tgbotapi.Message, bot *tgbotapi.BotAPI) (err error) { 20 | if message.ReplyToMessage == nil { 21 | msg := tgbotapi.NewMessage(message.Chat.ID, "请回复一条语音留言") 22 | msg.ReplyToMessageID = message.MessageID 23 | _, err = bot.Send(msg) 24 | return err 25 | } 26 | if message.ReplyToMessage.Voice == nil { 27 | msg := tgbotapi.NewMessage(message.Chat.ID, "请回复一条语音留言") 28 | msg.ReplyToMessageID = message.ReplyToMessage.MessageID 29 | _, err = bot.Send(msg) 30 | return err 31 | } 32 | 33 | tempBot := tgbotapi.BotAPI{ 34 | Token: bot.Token, 35 | Client: &http.Client{}, 36 | } 37 | tempBot.SetAPIEndpoint(tgbotapi.APIEndpoint) 38 | url, err := tempBot.GetFileDirectURL(message.ReplyToMessage.Voice.FileID) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | buf, err := http.Get(url) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | fileName := fmt.Sprintf("%d-%d-%d.ogg", message.ReplyToMessage.Chat.ID, message.ReplyToMessage.MessageID, time.Now().Unix()) 49 | file, err := os.OpenFile(fmt.Sprintf("%s/%s", cacheDir, fileName), os.O_CREATE|os.O_WRONLY, 0666) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | _, err = io.Copy(file, buf.Body) 55 | if err != nil { 56 | return err 57 | } 58 | file.Close() 59 | 60 | // convert ogg to mp3 61 | cmd := exec.Command("ffmpeg", "-i", fmt.Sprintf("%s/%s", cacheDir, fileName), fmt.Sprintf("%s/%s.mp3", cacheDir, fileName)) 62 | err = cmd.Run() 63 | if err != nil { 64 | //return err 65 | } 66 | _, err = os.Stat(fmt.Sprintf("%s/%s.mp3", cacheDir, fileName)) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | newFile, err := os.Open(fmt.Sprintf("%s/%s.mp3", cacheDir, fileName)) 72 | newBuf, _ := io.ReadAll(newFile) 73 | 74 | resp, err := uploadFile(recognizeAPI, newBuf) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | var result RecognizeResultData 80 | err = json.Unmarshal(resp, &result) 81 | 82 | if err != nil || len(result.Data.Result) == 0 { 83 | msg := tgbotapi.NewMessage(message.Chat.ID, "识别失败,可能是录音时间太短") 84 | msg.ReplyToMessageID = message.ReplyToMessage.MessageID 85 | _, _ = bot.Send(msg) 86 | return err 87 | } 88 | 89 | musicID := result.Data.Result[0].Song.Id 90 | msg := tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("https://music.163.com/song/%d", musicID)) 91 | msg.ReplyToMessageID = message.ReplyToMessage.MessageID 92 | _, _ = bot.Send(msg) 93 | 94 | return processMusic(musicID, *message.ReplyToMessage, bot) 95 | } 96 | 97 | func uploadFile(url string, file []byte) ([]byte, error) { 98 | bodyBuf := &bytes.Buffer{} 99 | bodyWriter := multipart.NewWriter(bodyBuf) 100 | fileWriter, err := bodyWriter.CreateFormFile("file", "") 101 | if err != nil { 102 | return nil, err 103 | } 104 | _, err = fileWriter.Write(file) 105 | if err != nil { 106 | return nil, err 107 | } 108 | contentType := bodyWriter.FormDataContentType() 109 | bodyWriter.Close() 110 | resp, err := http.Post(url, contentType, bodyBuf) 111 | if err != nil { 112 | return nil, err 113 | } 114 | defer resp.Body.Close() 115 | respBody, err := io.ReadAll(resp.Body) 116 | return respBody, nil 117 | } 118 | 119 | type RecognizeResultData struct { 120 | Data struct { 121 | Result []struct { 122 | Song struct { 123 | Name string `json:"name"` 124 | Id int `json:"id"` 125 | } `json:"song"` 126 | } `json:"result"` 127 | } `json:"data"` 128 | Code int `json:"code"` 129 | Message string `json:"message"` 130 | } 131 | -------------------------------------------------------------------------------- /bot/processLyric.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/XiaoMengXinX/Music163Api-Go/api" 11 | "github.com/XiaoMengXinX/Music163Api-Go/types" 12 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func processLyric(message tgbotapi.Message, bot *tgbotapi.BotAPI) (err error) { 17 | var msgResult tgbotapi.Message 18 | sendFailed := func() { 19 | editMsg := tgbotapi.NewEditMessageText(msgResult.Chat.ID, msgResult.MessageID, fmt.Sprintf(getLrcFailed)) 20 | _, err = bot.Send(editMsg) 21 | if err != nil { 22 | logrus.Errorln(err) 23 | } 24 | } 25 | if message.CommandArguments() == "" && message.ReplyToMessage == nil { 26 | msg := tgbotapi.NewMessage(message.Chat.ID, inputContent) 27 | msg.ReplyToMessageID = message.MessageID 28 | _, err = bot.Send(msg) 29 | return err 30 | } else if message.CommandArguments() == "" && message.ReplyToMessage != nil { 31 | message = *message.ReplyToMessage 32 | if !message.IsCommand() && len(message.Entities) != 0 { 33 | message.Entities[0].Type = "bot_command" 34 | message.Entities[0].Length = -1 35 | message.Entities[0].Offset = 0 36 | } else if !message.IsCommand() && len(message.Entities) == 0 { 37 | message.Entities = []tgbotapi.MessageEntity{{Type: "bot_command", Length: -1, Offset: 0}} 38 | } 39 | } 40 | msg := tgbotapi.NewMessage(message.Chat.ID, fetchingLyric) 41 | msg.ReplyToMessageID = message.MessageID 42 | msgResult, err = bot.Send(msg) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | musicID := parseMusicID(message.CommandArguments()) 48 | if musicID == 0 { 49 | searchResult, _ := api.SearchSong(data, api.SearchSongConfig{ 50 | Keyword: message.CommandArguments(), 51 | Limit: 5, 52 | }) 53 | if len(searchResult.Result.Songs) == 0 { 54 | editMsg := tgbotapi.NewEditMessageText(msgResult.Chat.ID, msgResult.MessageID, noResults) 55 | _, err = bot.Send(editMsg) 56 | return err 57 | } 58 | musicID = searchResult.Result.Songs[0].Id 59 | } 60 | 61 | b := api.NewBatch(api.BatchAPI{ 62 | Key: api.SongLyricAPI, 63 | Json: api.CreateSongLyricReqJson(musicID), 64 | }, api.BatchAPI{ 65 | Key: api.SongDetailAPI, 66 | Json: api.CreateSongDetailReqJson([]int{musicID}), 67 | }).Do(data) 68 | if b.Error != nil { 69 | sendFailed() 70 | return b.Error 71 | } 72 | 73 | _, result := b.Parse() 74 | var lyric types.SongLyricData 75 | var detail types.SongsDetailData 76 | _ = json.Unmarshal([]byte(result[api.SongLyricAPI]), &lyric) 77 | _ = json.Unmarshal([]byte(result[api.SongDetailAPI]), &detail) 78 | 79 | if lyric.Lrc.Lyric != "" && len(detail.Songs) != 0 { 80 | var replacer = strings.NewReplacer("/", " ", "?", " ", "*", " ", ":", " ", "|", " ", "\\", " ", "<", " ", ">", " ", "\"", " ") 81 | lrcPath := fmt.Sprintf("%s/%s - %s.lrc", cacheDir, replacer.Replace(parseArtist(detail.Songs[0])), replacer.Replace(detail.Songs[0].Name)) 82 | file, err := os.OpenFile(lrcPath, os.O_WRONLY|os.O_CREATE, 0666) 83 | if err != nil { 84 | sendFailed() 85 | return err 86 | } else { 87 | defer func(file *os.File) { 88 | _ = file.Close() 89 | }(file) 90 | write := bufio.NewWriter(file) 91 | _, _ = write.WriteString(lyric.Lrc.Lyric) 92 | err = write.Flush() 93 | if err != nil { 94 | sendFailed() 95 | return err 96 | } 97 | } 98 | defer func(name string) { 99 | err := os.Remove(name) 100 | if err != nil { 101 | logrus.Errorln(err) 102 | } 103 | }(lrcPath) 104 | var newFile tgbotapi.DocumentConfig 105 | newFile = tgbotapi.NewDocument(message.Chat.ID, tgbotapi.FilePath(lrcPath)) 106 | newFile.ReplyToMessageID = message.MessageID 107 | _, err = bot.Send(newFile) 108 | if err != nil { 109 | return err 110 | } 111 | deleteMsg := tgbotapi.NewDeleteMessage(msgResult.Chat.ID, msgResult.MessageID) 112 | _, err = bot.Request(deleteMsg) 113 | return err 114 | } 115 | sendFailed() 116 | return 117 | } 118 | -------------------------------------------------------------------------------- /bot/modules.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/XiaoMengXinX/Music163Api-Go/api" 10 | "github.com/go-telegram-bot-api/telegram-bot-api/v5" 11 | "gorm.io/gorm" 12 | "gorm.io/gorm/logger" 13 | ) 14 | 15 | // 限制查询速度及并发 16 | var statLimiter = make(chan bool, 1) 17 | 18 | func printAbout(message tgbotapi.Message, bot *tgbotapi.BotAPI) (err error) { 19 | if config["VersionName"] == "" { 20 | config["VersionName"] = config["BinVersionName"] 21 | } 22 | newMsg := tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf(aboutText, config["runtimeVer"], config["BinVersionName"], config["commitSHA"], config["buildTime"], config["buildArch"])) 23 | newMsg.ParseMode = tgbotapi.ModeMarkdown 24 | newMsg.ReplyToMessageID = message.MessageID 25 | message, err = bot.Send(newMsg) 26 | if err != nil { 27 | return err 28 | } 29 | return err 30 | } 31 | 32 | func processCallbackMusic(args []string, updateQuery tgbotapi.CallbackQuery, bot *tgbotapi.BotAPI) (err error) { 33 | musicID, _ := strconv.Atoi(args[1]) 34 | if updateQuery.Message.Chat.IsPrivate() { 35 | callback := tgbotapi.NewCallback(updateQuery.ID, callbackText) 36 | _, err = bot.Request(callback) 37 | if err != nil { 38 | return err 39 | } 40 | message := *updateQuery.Message 41 | return processMusic(musicID, message, bot) 42 | } 43 | callback := tgbotapi.NewCallback(updateQuery.ID, callbackText) 44 | callback.URL = fmt.Sprintf("t.me/%s?start=%d", botName, musicID) 45 | _, err = bot.Request(callback) 46 | return err 47 | } 48 | 49 | func processRmCache(message tgbotapi.Message, bot *tgbotapi.BotAPI) (err error) { 50 | musicID := parseMusicID(message.CommandArguments()) 51 | if musicID == 0 { 52 | musicID = parseProgramID(message.CommandArguments()) 53 | if musicID = getProgramRealID(musicID); musicID == 0 { 54 | return err 55 | } 56 | } 57 | db := MusicDB.Session(&gorm.Session{}) 58 | var songInfo SongInfo 59 | err = db.Where("music_id = ?", musicID).First(&songInfo).Error 60 | if !errors.Is(err, logger.ErrRecordNotFound) { 61 | db.Where("music_id = ?", musicID).Delete(&songInfo) 62 | newMsg := tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf(rmcacheReport, songInfo.SongName)) 63 | newMsg.ReplyToMessageID = message.MessageID 64 | message, err = bot.Send(newMsg) 65 | if err != nil { 66 | return err 67 | } 68 | return err 69 | } 70 | newMsg := tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf(noCache)) 71 | newMsg.ReplyToMessageID = message.MessageID 72 | message, err = bot.Send(newMsg) 73 | return err 74 | } 75 | 76 | func processAnyMusic(message tgbotapi.Message, bot *tgbotapi.BotAPI) (err error) { 77 | if message.CommandArguments() == "" { 78 | msg := tgbotapi.NewMessage(message.Chat.ID, inputIDorKeyword) 79 | msg.ReplyToMessageID = message.MessageID 80 | _, err = bot.Send(msg) 81 | return 82 | } 83 | musicID, _ := strconv.Atoi(message.CommandArguments()) 84 | if musicID != 0 { 85 | err = processMusic(musicID, message, bot) 86 | return err 87 | } 88 | searchResult, _ := api.SearchSong(data, api.SearchSongConfig{ 89 | Keyword: message.CommandArguments(), 90 | Limit: 10, 91 | }) 92 | if len(searchResult.Result.Songs) == 0 { 93 | msg := tgbotapi.NewMessage(message.Chat.ID, noResults) 94 | msg.ReplyToMessageID = message.MessageID 95 | _, err = bot.Send(msg) 96 | return err 97 | } 98 | err = processMusic(searchResult.Result.Songs[0].Id, message, bot) 99 | return err 100 | } 101 | 102 | func processStatus(message tgbotapi.Message, bot *tgbotapi.BotAPI) (err error) { 103 | statLimiter <- true 104 | defer func() { 105 | time.Sleep(time.Millisecond * 500) 106 | <-statLimiter 107 | }() 108 | db := MusicDB.Session(&gorm.Session{}) 109 | var fromCount, chatCount int64 110 | var lastRecord SongInfo 111 | db.Where("from_user_id = ?", message.From.ID).Count(&fromCount) 112 | db.Where("from_chat_id = ?", message.Chat.ID).Count(&chatCount) 113 | db.Last(&lastRecord) 114 | 115 | var chatInfo string 116 | if message.Chat.UserName != "" && message.Chat.Title == "" { 117 | chatInfo = fmt.Sprintf("[%s](tg://user?id=%d)", mdV2Replacer.Replace(message.Chat.UserName), message.Chat.ID) 118 | } else if message.Chat.UserName != "" { 119 | chatInfo = fmt.Sprintf("[%s](https://t.me/%s)", mdV2Replacer.Replace(message.Chat.Title), message.Chat.UserName) 120 | } else { 121 | chatInfo = fmt.Sprintf("%s", mdV2Replacer.Replace(message.Chat.Title)) 122 | } 123 | msgText := fmt.Sprintf(statusInfo, lastRecord.ID, chatInfo, chatCount, message.From.ID, message.From.ID, fromCount) 124 | msg := tgbotapi.NewMessage(message.Chat.ID, msgText) 125 | msg.ReplyToMessageID = message.MessageID 126 | msg.ParseMode = tgbotapi.ModeMarkdownV2 127 | _, err = bot.Send(msg) 128 | return err 129 | } 130 | -------------------------------------------------------------------------------- /bot/logger.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path" 9 | "strings" 10 | "time" 11 | 12 | "github.com/sirupsen/logrus" 13 | "gorm.io/gorm/logger" 14 | ) 15 | 16 | // Colors 17 | const ( 18 | Reset = "\033[0m" 19 | Red = "\033[31m" 20 | Green = "\033[32m" 21 | Yellow = "\033[33m" 22 | Blue = "\033[34m" 23 | Magenta = "\033[35m" 24 | Cyan = "\033[36m" 25 | White = "\033[37m" 26 | BlueBold = "\033[34;1m" 27 | MagentaBold = "\033[35;1m" 28 | RedBold = "\033[31;1m" 29 | YellowBold = "\033[33;1m" 30 | ) 31 | 32 | type LogFormatter struct{} 33 | 34 | func (s *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) { 35 | timestamp := time.Now().Local().Format("2006/01/02 15:04:05") 36 | var msg string 37 | msg = fmt.Sprintf("%s [%s] %s (%s:%d)\n", timestamp, strings.ToUpper(entry.Level.String()), entry.Message, path.Base(entry.Caller.File), entry.Caller.Line) 38 | return []byte(msg), nil 39 | } 40 | 41 | func InitLogger(l *logrus.Logger) { 42 | l.SetFormatter(&logrus.TextFormatter{ 43 | DisableColors: false, 44 | FullTimestamp: true, 45 | DisableLevelTruncation: true, 46 | PadLevelText: true, 47 | }) 48 | l.SetFormatter(new(LogFormatter)) 49 | l.SetReportCaller(true) 50 | l.SetOutput(os.Stdout) 51 | } 52 | 53 | type LogInterface interface { 54 | Info(context.Context, string, ...interface{}) 55 | Warn(context.Context, string, ...interface{}) 56 | Error(context.Context, string, ...interface{}) 57 | Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) 58 | LogMode(level logger.LogLevel) logger.Interface 59 | } 60 | 61 | type dbLogger struct { 62 | *logrus.Logger 63 | SlowThreshold time.Duration 64 | traceStr, traceErrStr, traceWarnStr string 65 | } 66 | 67 | func NewLogger(level logger.LogLevel) logger.Interface { 68 | traceStr := Yellow + "[%.3fms] " + BlueBold + "[rows:%v]" + Reset + " %s" 69 | traceWarnStr := Green + "%s " + Yellow + "%s\n" + Reset + RedBold + "[%.3fms] " + Yellow + "[rows:%v]" + Magenta + " %s" + Reset 70 | traceErrStr := RedBold + "%s " + MagentaBold + "%s\n" + Reset + Yellow + "[%.3fms] " + BlueBold + "[rows:%v]" + Reset + " %s" 71 | l := &dbLogger{ 72 | Logger: logrus.New(), 73 | SlowThreshold: 200 * time.Millisecond, 74 | traceStr: traceStr, 75 | traceWarnStr: traceWarnStr, 76 | traceErrStr: traceErrStr, 77 | } 78 | InitLogger(l.Logger) 79 | l.LogMode(level) 80 | return l 81 | } 82 | 83 | func (l *dbLogger) Info(ctx context.Context, msg string, data ...interface{}) { 84 | if int(l.GetLevel()) >= int(logger.Info) { 85 | l.Infof(msg, data...) 86 | } 87 | } 88 | 89 | func (l *dbLogger) Warn(ctx context.Context, msg string, data ...interface{}) { 90 | if int(l.GetLevel()) >= int(logger.Warn) { 91 | l.Warnf(msg, data...) 92 | } 93 | } 94 | 95 | func (l *dbLogger) Error(ctx context.Context, msg string, data ...interface{}) { 96 | if int(l.GetLevel()) >= int(logger.Error) { 97 | l.Errorf(msg, data...) 98 | } 99 | } 100 | 101 | func (l *dbLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { 102 | if int(l.Level) <= int(logger.Silent) { 103 | return 104 | } 105 | 106 | elapsed := time.Since(begin) 107 | switch { 108 | case err != nil && int(l.Level) >= int(logger.Error) && (!errors.Is(err, logger.ErrRecordNotFound)): 109 | sql, rows := fc() 110 | if rows == -1 { 111 | l.Printf(l.traceErrStr, err, float64(elapsed.Nanoseconds())/1e6, "-", sql) 112 | } else { 113 | l.Printf(l.traceErrStr, err, float64(elapsed.Nanoseconds())/1e6, rows, sql) 114 | } 115 | case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && int(l.Level) >= int(logger.Warn): 116 | sql, rows := fc() 117 | slowLog := fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold) 118 | if rows == -1 { 119 | l.Printf(l.traceWarnStr, slowLog, float64(elapsed.Nanoseconds())/1e6, "-", sql) 120 | } else { 121 | l.Printf(l.traceWarnStr, slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql) 122 | } 123 | case int(l.Level) == int(logger.Info): 124 | sql, rows := fc() 125 | if rows == -1 { 126 | l.Printf(l.traceStr, float64(elapsed.Nanoseconds())/1e6, "-", sql) 127 | } else { 128 | l.Printf(l.traceStr, float64(elapsed.Nanoseconds())/1e6, rows, sql) 129 | } 130 | } 131 | return 132 | } 133 | 134 | func (l *dbLogger) LogMode(level logger.LogLevel) logger.Interface { 135 | switch level { 136 | case logger.Silent: 137 | l.SetLevel(logrus.FatalLevel) 138 | case logger.Error: 139 | l.SetLevel(logrus.ErrorLevel) 140 | case logger.Warn: 141 | l.SetLevel(logrus.WarnLevel) 142 | case logger.Info: 143 | l.SetLevel(logrus.InfoLevel) 144 | } 145 | return l 146 | } 147 | -------------------------------------------------------------------------------- /bot/tools.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/XiaoMengXinX/Music163Api-Go/api" 16 | "github.com/XiaoMengXinX/Music163Api-Go/types" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | // 判断数组包含关系 21 | func in(target string, strArray []string) bool { 22 | sort.Strings(strArray) 23 | index := sort.SearchStrings(strArray, target) 24 | if index < len(strArray) && strArray[index] == target { 25 | return true 26 | } 27 | return false 28 | } 29 | 30 | // 解析作曲家信息 31 | func parseArtist(songDetail types.SongDetailData) string { 32 | var artists string 33 | for i, ar := range songDetail.Ar { 34 | if i == 0 { 35 | artists = ar.Name 36 | } else { 37 | artists = fmt.Sprintf("%s/%s", artists, ar.Name) 38 | } 39 | } 40 | return artists 41 | } 42 | 43 | // 判断文件夹是否存在/新建文件夹 44 | func dirExists(path string) bool { 45 | _, err := os.Stat(path) 46 | if err == nil { 47 | return true 48 | } 49 | if os.IsNotExist(err) { 50 | err := os.Mkdir(path, os.ModePerm) 51 | if err != nil { 52 | logrus.Errorf("mkdir %v failed: %v\n", path, err) 53 | } 54 | return false 55 | } 56 | logrus.Errorf("Error: %v\n", err) 57 | return false 58 | } 59 | 60 | // 校验 md5 61 | func verifyMD5(filePath string, md5str string) (bool, error) { 62 | f, err := os.Open(filePath) 63 | if err != nil { 64 | return false, err 65 | } 66 | defer f.Close() 67 | md5hash := md5.New() 68 | if _, err := io.Copy(md5hash, f); err != nil { 69 | return false, err 70 | } 71 | if hex.EncodeToString(md5hash.Sum(nil)) != md5str { 72 | return false, fmt.Errorf(md5VerFailed) 73 | } 74 | return true, nil 75 | } 76 | 77 | // 解析 MusicID 78 | func parseMusicID(text string) int { 79 | var replacer = strings.NewReplacer("\n", "", " ", "") 80 | messageText := replacer.Replace(text) 81 | musicUrl := regUrl.FindStringSubmatch(messageText) 82 | if len(musicUrl) != 0 { 83 | if strings.Contains(musicUrl[0], "song") { 84 | ur, _ := url.Parse(musicUrl[0]) 85 | id := ur.Query().Get("id") 86 | if musicid, _ := strconv.Atoi(id); musicid != 0 { 87 | return musicid 88 | } 89 | } 90 | } 91 | musicid, _ := strconv.Atoi(linkTestMusic(messageText)) 92 | return musicid 93 | } 94 | 95 | // 解析 ProgramID 96 | func parseProgramID(text string) int { 97 | var replacer = strings.NewReplacer("\n", "", " ", "") 98 | messageText := replacer.Replace(text) 99 | programid, _ := strconv.Atoi(linkTestProgram(messageText)) 100 | return programid 101 | } 102 | 103 | // 提取数字 104 | func extractInt(text string) string { 105 | matchArr := regInt.FindStringSubmatch(text) 106 | if len(matchArr) == 0 { 107 | return "" 108 | } 109 | return matchArr[0] 110 | } 111 | 112 | // 解析分享链接 113 | func linkTestMusic(text string) string { 114 | return extractInt(reg5.ReplaceAllString(reg4.ReplaceAllString(reg3.ReplaceAllString(reg2.ReplaceAllString(reg1.ReplaceAllString(text, ""), ""), ""), ""), "")) 115 | } 116 | 117 | func linkTestProgram(text string) string { 118 | return extractInt(reg5.ReplaceAllString(reg4.ReplaceAllString(reg3.ReplaceAllString(regP4.ReplaceAllString(regP3.ReplaceAllString(regP2.ReplaceAllString(regP1.ReplaceAllString(text, ""), ""), ""), ""), ""), ""), "")) 119 | } 120 | 121 | // 判断 error 是否为超时错误 122 | func isTimeout(err error) bool { 123 | if strings.Contains(fmt.Sprintf("%v", err), "context deadline exceeded") { 124 | return true 125 | } 126 | return false 127 | } 128 | 129 | // 获取电台节目的 MusicID 130 | func getProgramRealID(programID int) int { 131 | programDetail, err := api.GetProgramDetail(data, programID) 132 | if err != nil { 133 | return 0 134 | } 135 | if programDetail.Program.MainSong.ID != 0 { 136 | return programDetail.Program.MainSong.ID 137 | } 138 | return 0 139 | } 140 | 141 | // 获取重定向后的地址 142 | func getRedirectUrl(text string) (string) { 143 | var replacer = strings.NewReplacer("\n", "", " ", "") 144 | messageText := replacer.Replace(text) 145 | musicUrl := regUrl.FindStringSubmatch(messageText) 146 | if len(musicUrl) != 0 { 147 | if strings.Contains(musicUrl[0], "163cn.tv") { 148 | var url = musicUrl[0] 149 | // 创建新的请求 150 | req, err := http.NewRequest("GET", url, nil) 151 | if err != nil { 152 | return text 153 | } 154 | 155 | // 设置 CheckRedirect 函数来处理重定向 156 | client := &http.Client{ 157 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 158 | return http.ErrUseLastResponse 159 | }, 160 | } 161 | 162 | // 执行请求 163 | resp, err := client.Do(req) 164 | if err != nil { 165 | return text 166 | } 167 | defer resp.Body.Close() 168 | 169 | // 返回最终重定向的网址 170 | location := resp.Header.Get("location") 171 | return location 172 | } 173 | } 174 | return text 175 | } 176 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path" 10 | "runtime" 11 | "strings" 12 | "time" 13 | 14 | "github.com/XiaoMengXinX/Music163bot-Go/v2/bot" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | var config map[string]string 19 | 20 | var ( 21 | _ConfigPath *string 22 | _NoUpdate *bool 23 | _NoMD5Check *bool 24 | _SrcPath *string 25 | _BotEntry *string 26 | ) 27 | 28 | var ( 29 | runtimeVer = fmt.Sprintf(runtime.Version()) // 编译环境 30 | _VersionName = "" // 程序版本 31 | commitSHA = "" // 编译哈希 32 | buildTime = "" // 编译日期 33 | buildArch = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) // 运行环境 34 | repoPath = "" // 项目地址 35 | rawRepoPath = "" 36 | ) 37 | 38 | // LogFormatter 自定义 log 格式 39 | type LogFormatter struct{} 40 | 41 | // Format 自定义 log 格式 42 | func (s *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) { 43 | timestamp := time.Now().Local().Format("2006/01/02 15:04:05") 44 | var msg string 45 | msg = fmt.Sprintf("%s [%s] %s (%s:%d)\n", timestamp, strings.ToUpper(entry.Level.String()), entry.Message, path.Base(entry.Caller.File), entry.Caller.Line) 46 | return []byte(msg), nil 47 | } 48 | 49 | func init() { 50 | logrus.SetFormatter(&logrus.TextFormatter{ 51 | DisableColors: false, 52 | FullTimestamp: true, 53 | DisableLevelTruncation: true, 54 | PadLevelText: true, 55 | }) 56 | logrus.SetFormatter(new(LogFormatter)) 57 | logrus.SetReportCaller(true) 58 | dirExists("./log") 59 | timeStamp := time.Now().Local().Format("2006-01-02") 60 | logFile := fmt.Sprintf("./log/%v.log", timeStamp) 61 | file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 62 | if err != nil { 63 | logrus.Errorln(err) 64 | } 65 | output := io.MultiWriter(os.Stdout, file) 66 | logrus.SetOutput(output) 67 | if config["LogLevel"] != "" { 68 | level, err := logrus.ParseLevel(config["LogLevel"]) 69 | if err != nil { 70 | logrus.Errorln(err) 71 | } else { 72 | logrus.SetLevel(level) 73 | } 74 | } 75 | 76 | f := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 77 | _ConfigPath = f.String("c", "config.ini", "配置文件") 78 | _NoUpdate = f.Bool("no-update", false, "关闭更新检测") 79 | _NoMD5Check = f.Bool("no-md5-check", false, "关闭 md5 效验") 80 | _SrcPath = f.String("path", "./src", "自定义更新下载/加载路径") 81 | _BotEntry = f.String("bot-entry", "bot.Start", "自定义动态加载入口") 82 | _ = f.Parse(os.Args[1:]) 83 | 84 | logrus.Printf("Music163bot-Go %s", _VersionName) 85 | 86 | conf, err := readConfig(*_ConfigPath) 87 | if err != nil { 88 | logrus.Errorln("读取配置文件失败,请检查配置文件") 89 | logrus.Fatal(err) 90 | } 91 | config = conf 92 | initConfig(config) 93 | } 94 | 95 | func main() { 96 | var actionCode = -1 // actionCode: 0 exit, 1 error exit, 2 reload src, 3 update src 97 | 98 | for true { 99 | logrus.Printf("加载内置版本 %s 中", _VersionName) 100 | actionCode = bot.Start(config) 101 | switch actionCode { 102 | case 0: 103 | os.Exit(0) 104 | case 1: 105 | logrus.Fatal("Unexpected error") 106 | case 2: 107 | time.Sleep(2 * time.Second) 108 | conf, err := readConfig(*_ConfigPath) 109 | if err != nil { 110 | logrus.Errorln(err) 111 | logrus.Fatal("读取配置文件失败,请检查配置文件") 112 | } else { 113 | config = conf 114 | initConfig(config) 115 | } 116 | continue 117 | case 3: 118 | time.Sleep(2 * time.Second) 119 | continue 120 | } 121 | } 122 | } 123 | 124 | func readConfig(path string) (config map[string]string, err error) { 125 | config = make(map[string]string) 126 | f, err := os.Open(path) 127 | if err != nil { 128 | return config, err 129 | } 130 | defer func(f *os.File) { 131 | e := f.Close() 132 | if e != nil { 133 | err = e 134 | } 135 | }(f) 136 | r := bufio.NewReader(f) 137 | for { 138 | b, _, err := r.ReadLine() 139 | if err != nil { 140 | if err == io.EOF { 141 | break 142 | } 143 | return config, err 144 | } 145 | s := strings.TrimSpace(string(b)) 146 | index := strings.Index(s, "=") 147 | if index < 0 { 148 | continue 149 | } 150 | key := strings.TrimSpace(s[:index]) 151 | if len(key) == 0 { 152 | continue 153 | } 154 | value := strings.TrimSpace(s[index+1:]) 155 | if len(value) == 0 { 156 | continue 157 | } 158 | config[key] = value 159 | } 160 | return config, err 161 | } 162 | 163 | func dirExists(path string) bool { 164 | _, err := os.Stat(path) 165 | if err == nil { 166 | return true 167 | } 168 | if os.IsNotExist(err) { 169 | err := os.Mkdir(path, os.ModePerm) 170 | if err != nil { 171 | logrus.Errorf("mkdir %v failed: %v\n", path, err) 172 | } 173 | return false 174 | } 175 | logrus.Errorf("Error: %v\n", err) 176 | return false 177 | } 178 | 179 | func initConfig(config map[string]string) { 180 | config["BinVersionName"] = _VersionName 181 | config["runtimeVer"] = runtimeVer 182 | config["buildTime"] = buildTime 183 | config["commitSHA"] = commitSHA 184 | config["buildArch"] = buildArch 185 | config["repoPath"] = repoPath 186 | config["rawRepoPath"] = rawRepoPath 187 | if *_NoUpdate { 188 | config["AutoUpdate"] = "false" 189 | } else if config["AutoUpdate"] == "false" { 190 | *_NoUpdate = true 191 | } 192 | if *_NoMD5Check { 193 | config["CheckMD5"] = "false" 194 | } else if config["CheckMD5"] == "false" { 195 | *_NoMD5Check = true 196 | } 197 | if config["SrcPath"] != "" { 198 | *_SrcPath = config["SrcPath"] 199 | } else { 200 | config["SrcPath"] = *_SrcPath 201 | } 202 | if config["BotEntry"] != "" { 203 | *_BotEntry = config["BotEntry"] 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /bot/processInline.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/XiaoMengXinX/Music163Api-Go/api" 9 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | func processInlineMusic(musicid int, query tgbotapi.InlineQuery, bot *tgbotapi.BotAPI) (err error) { 14 | var songInfo SongInfo 15 | db := MusicDB.Session(&gorm.Session{}) 16 | err = db.Where("music_id = ?", musicid).First(&songInfo).Error // 查找是否有缓存数据 17 | if err == nil { // 从缓存数据回应 inlineQuery 18 | if songInfo.FileID != "" && songInfo.SongName != "" { 19 | var numericKeyboard tgbotapi.InlineKeyboardMarkup 20 | numericKeyboard = tgbotapi.NewInlineKeyboardMarkup( 21 | tgbotapi.NewInlineKeyboardRow( 22 | tgbotapi.NewInlineKeyboardButtonURL(fmt.Sprintf("%s- %s", songInfo.SongName, songInfo.SongArtists), fmt.Sprintf("https://music.163.com/song?id=%d", songInfo.MusicID)), 23 | ), 24 | tgbotapi.NewInlineKeyboardRow( 25 | tgbotapi.NewInlineKeyboardButtonSwitch(sendMeTo, fmt.Sprintf("https://music.163.com/song?id=%d", songInfo.MusicID)), 26 | ), 27 | ) 28 | 29 | newAudio := tgbotapi.NewInlineQueryResultCachedDocument(query.ID, songInfo.FileID, fmt.Sprintf("%s - %s", songInfo.SongArtists, songInfo.SongName)) 30 | newAudio.Caption = fmt.Sprintf(musicInfo, songInfo.SongName, songInfo.SongArtists, songInfo.SongAlbum, songInfo.FileExt, float64(songInfo.MusicSize+songInfo.EmbPicSize)/1024/1024, float64(songInfo.BitRate)/1000, botName) 31 | newAudio.ReplyMarkup = &numericKeyboard 32 | newAudio.Description = songInfo.SongAlbum 33 | 34 | inlineConf := tgbotapi.InlineConfig{ 35 | InlineQueryID: query.ID, 36 | Results: []interface{}{newAudio}, 37 | IsPersonal: false, 38 | CacheTime: 3600, 39 | } 40 | 41 | _, err := bot.Request(inlineConf) 42 | if err != nil { 43 | return err 44 | } 45 | } 46 | } else { 47 | inlineMsg := tgbotapi.NewInlineQueryResultArticle(query.ID, noCache, query.Query) 48 | inlineMsg.Description = tapToDownload 49 | 50 | inlineConf := tgbotapi.InlineConfig{ 51 | InlineQueryID: query.ID, 52 | IsPersonal: false, 53 | Results: []interface{}{inlineMsg}, 54 | CacheTime: 60, 55 | SwitchPMText: tapMeToDown, 56 | SwitchPMParameter: fmt.Sprintf("%d", musicid), 57 | } 58 | 59 | _, err := bot.Request(inlineConf) 60 | if err != nil { 61 | return err 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | func processEmptyInline(message tgbotapi.InlineQuery, bot *tgbotapi.BotAPI) (err error) { 68 | inlineMsg := tgbotapi.NewInlineQueryResultArticle(message.ID, "输入 help 获取帮助", "Music163bot-Go v2") 69 | inlineConf := tgbotapi.InlineConfig{ 70 | InlineQueryID: message.ID, 71 | IsPersonal: false, 72 | Results: []interface{}{inlineMsg}, 73 | CacheTime: 3600, 74 | } 75 | _, err = bot.Request(inlineConf) 76 | if err != nil { 77 | return err 78 | } 79 | return err 80 | } 81 | 82 | func processInlineHelp(message tgbotapi.InlineQuery, bot *tgbotapi.BotAPI) (err error) { 83 | randomID := time.Now().UnixMicro() 84 | inlineMsg1 := tgbotapi.NewInlineQueryResultArticle(fmt.Sprintf("%d", randomID), "1.粘贴音乐分享URL或输入MusicID", "Music163bot-Go v2") 85 | inlineMsg2 := tgbotapi.NewInlineQueryResultArticle(fmt.Sprintf("%d", randomID+1), "2.输入 search+关键词 搜索歌曲", "Music163bot-Go v2") 86 | inlineConf := tgbotapi.InlineConfig{ 87 | InlineQueryID: message.ID, 88 | IsPersonal: false, 89 | Results: []interface{}{inlineMsg1, inlineMsg2}, 90 | CacheTime: 3600, 91 | } 92 | _, err = bot.Request(inlineConf) 93 | if err != nil { 94 | return err 95 | } 96 | return err 97 | } 98 | 99 | func processInlineSearch(message tgbotapi.InlineQuery, bot *tgbotapi.BotAPI) (err error) { 100 | randomID := time.Now().UnixMicro() 101 | keyWord := strings.Replace(message.Query, "search", "", 1) 102 | if keyWord == "" { 103 | inlineMsg := tgbotapi.NewInlineQueryResultArticle(fmt.Sprintf("%d", randomID), "请输入关键词", "Music163bot-Go v2") 104 | inlineConf := tgbotapi.InlineConfig{ 105 | InlineQueryID: message.ID, 106 | IsPersonal: false, 107 | Results: []interface{}{inlineMsg}, 108 | CacheTime: 3600, 109 | } 110 | _, err = bot.Request(inlineConf) 111 | return err 112 | } 113 | result, err := api.SearchSong(data, api.SearchSongConfig{ 114 | Keyword: keyWord, 115 | }) 116 | if err != nil { 117 | return err 118 | } 119 | searchResult := result 120 | if len(searchResult.Result.Songs) == 0 { 121 | inlineMsg := tgbotapi.NewInlineQueryResultArticle(fmt.Sprintf("%d", randomID), noResults, noResults) 122 | inlineConf := tgbotapi.InlineConfig{ 123 | InlineQueryID: message.ID, 124 | IsPersonal: false, 125 | Results: []interface{}{inlineMsg}, 126 | CacheTime: 3600, 127 | } 128 | _, err = bot.Request(inlineConf) 129 | return err 130 | } 131 | var inlineMsgs []interface{} 132 | for i := 0; i < len(searchResult.Result.Songs) && i < 10; i++ { 133 | var songArtists string 134 | for i, artist := range searchResult.Result.Songs[i].Artists { 135 | if i == 0 { 136 | songArtists = artist.Name 137 | } else { 138 | songArtists = fmt.Sprintf("%s/%s", songArtists, artist.Name) 139 | } 140 | } 141 | inlineMsg := tgbotapi.NewInlineQueryResultArticle(fmt.Sprintf("%d", randomID+int64(i)), searchResult.Result.Songs[i].Name, fmt.Sprintf("/netease %d", searchResult.Result.Songs[i].Id)) 142 | inlineMsg.Description = songArtists 143 | inlineMsgs = append(inlineMsgs, inlineMsg) 144 | } 145 | inlineConf := tgbotapi.InlineConfig{ 146 | InlineQueryID: message.ID, 147 | IsPersonal: false, 148 | Results: inlineMsgs, 149 | CacheTime: 3600, 150 | } 151 | _, err = bot.Request(inlineConf) 152 | return err 153 | } 154 | -------------------------------------------------------------------------------- /bot/bot.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/XiaoMengXinX/Music163Api-Go/utils" 10 | "github.com/go-telegram-bot-api/telegram-bot-api/v5" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // Start bot entry 15 | func Start(conf map[string]string) (actionCode int) { 16 | config = conf 17 | defer func() { 18 | e := recover() 19 | if e != nil { 20 | logrus.Errorln(e) 21 | actionCode = 1 22 | } 23 | }() 24 | // 创建缓存文件夹 25 | dirExists(cacheDir) 26 | 27 | // 解析 bot 管理员配置 28 | botAdminStr = strings.Split(config["BotAdmin"], ",") 29 | if len(botAdminStr) == 0 && config["BotAdmin"] != "" { 30 | botAdminStr = []string{config["BotAdmin"]} 31 | } 32 | if len(botAdminStr) != 0 { 33 | for _, s := range botAdminStr { 34 | id, err := strconv.Atoi(s) 35 | if err == nil { 36 | botAdmin = append(botAdmin, id) 37 | } 38 | } 39 | } 40 | 41 | // 初始化数据库 42 | err := initDB(config) 43 | if err != nil { 44 | logrus.Errorln(err) 45 | return 1 46 | } 47 | db, _ := MusicDB.DB() 48 | db.SetMaxOpenConns(1) 49 | defer db.Close() 50 | 51 | if config["MUSIC_U"] != "" { 52 | data = utils.RequestData{ 53 | Cookies: []*http.Cookie{ 54 | { 55 | Name: "MUSIC_U", 56 | Value: config["MUSIC_U"], 57 | }, 58 | }, 59 | } 60 | } 61 | if config["BotAPI"] != "" { 62 | botAPI = config["BotAPI"] 63 | } 64 | 65 | if maxRetryTimes, _ = strconv.Atoi(config["MaxRetryTimes"]); maxRetryTimes <= 0 { 66 | maxRetryTimes = 3 67 | } 68 | if downloaderTimeout, _ = strconv.Atoi(config["DownloadTimeout"]); downloaderTimeout <= 0 { 69 | downloaderTimeout = 60 70 | } 71 | 72 | // 设置 bot 日志接口 73 | err = tgbotapi.SetLogger(logrus.StandardLogger()) 74 | if err != nil { 75 | logrus.Errorln(err) 76 | return 1 77 | } 78 | // 配置 token、api、debug 79 | bot, err = tgbotapi.NewBotAPIWithAPIEndpoint(config["BOT_TOKEN"], botAPI+"/bot%s/%s") 80 | if err != nil { 81 | logrus.Errorln(err) 82 | return 1 83 | } 84 | if config["BotDebug"] == "true" { 85 | bot.Debug = true 86 | } 87 | 88 | logrus.Printf("%s 验证成功", bot.Self.UserName) 89 | botName = bot.Self.UserName 90 | 91 | u := tgbotapi.NewUpdate(0) 92 | u.Timeout = 60 93 | 94 | updates := bot.GetUpdatesChan(u) 95 | defer bot.StopReceivingUpdates() 96 | 97 | for update := range updates { 98 | if update.Message == nil && update.CallbackQuery == nil && update.InlineQuery == nil { // ignore any non-Message Updates 99 | continue 100 | } 101 | switch { 102 | case update.Message != nil: 103 | updateMsg := *update.Message 104 | if atStr := strings.ReplaceAll(update.Message.CommandWithAt(), update.Message.Command(), ""); update.Message.Command() != "" && (atStr == "" || atStr == "@"+botName) { 105 | switch update.Message.Command() { 106 | case "start": 107 | if !updateMsg.Chat.IsPrivate() { 108 | return 109 | } 110 | go func() { 111 | musicID, _ := strconv.Atoi(updateMsg.CommandArguments()) 112 | if musicID == 0 { 113 | return 114 | } 115 | err := processMusic(musicID, updateMsg, bot) 116 | if err != nil { 117 | logrus.Errorln(err) 118 | } 119 | }() 120 | case "music", "netease": 121 | go func() { 122 | err := processAnyMusic(updateMsg, bot) 123 | if err != nil { 124 | logrus.Errorln(err) 125 | } 126 | }() 127 | case "program": 128 | go func() { 129 | id, _ := strconv.Atoi(updateMsg.CommandArguments()) 130 | musicID := getProgramRealID(id) 131 | if musicID != 0 { 132 | err := processMusic(musicID, updateMsg, bot) 133 | if err != nil { 134 | logrus.Errorln(err) 135 | } 136 | } 137 | }() 138 | case "lyric": 139 | go func() { 140 | err := processLyric(updateMsg, bot) 141 | if err != nil { 142 | logrus.Errorln(err) 143 | } 144 | }() 145 | case "search": 146 | go func() { 147 | err := processSearch(updateMsg, bot) 148 | if err != nil { 149 | logrus.Errorln(err) 150 | } 151 | }() 152 | case "recognize": 153 | go func() { 154 | err := recognizeMusic(updateMsg, bot) 155 | if err != nil { 156 | logrus.Errorln(err) 157 | } 158 | }() 159 | case "about": 160 | go func() { 161 | err := printAbout(updateMsg, bot) 162 | if err != nil { 163 | logrus.Errorln(err) 164 | } 165 | }() 166 | case "status": 167 | go func() { 168 | err := processStatus(updateMsg, bot) 169 | if err != nil { 170 | logrus.Errorln(err) 171 | } 172 | }() 173 | } 174 | if in(fmt.Sprintf("%d", update.Message.From.ID), botAdminStr) { 175 | switch update.Message.Command() { 176 | case "rmcache": 177 | go func() { 178 | err := processRmCache(updateMsg, bot) 179 | if err != nil { 180 | logrus.Errorln(err) 181 | } 182 | }() 183 | case "reload": 184 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, reloading) 185 | msg.ReplyToMessageID = update.Message.MessageID 186 | _, _ = bot.Send(msg) 187 | return 2 188 | } 189 | } 190 | } else if strings.Contains(update.Message.Text, "music.163.com") || strings.Contains(update.Message.Text, "163cn.tv") || strings.Contains(update.Message.Text, "163cn.link") { 191 | go func() { 192 | var text = update.Message.Text 193 | if strings.Contains(update.Message.Text, "163cn.tv") || strings.Contains(update.Message.Text, "163cn.link") { 194 | text = getRedirectUrl(text) 195 | } 196 | id := parseMusicID(text) 197 | if id != 0 { 198 | err := processMusic(id, updateMsg, bot) 199 | if err != nil { 200 | logrus.Errorln(err) 201 | } 202 | } else if id = parseProgramID(text); id != 0 { 203 | if id = getProgramRealID(id); id != 0 { 204 | err := processMusic(id, updateMsg, bot) 205 | if err != nil { 206 | logrus.Errorln(err) 207 | } 208 | } 209 | } 210 | }() 211 | } 212 | case update.CallbackQuery != nil: 213 | updateQuery := *update.CallbackQuery 214 | args := strings.Split(updateQuery.Data, " ") 215 | if len(args) < 2 { 216 | continue 217 | } 218 | switch args[0] { 219 | case "music": 220 | go func() { 221 | err := processCallbackMusic(args, updateQuery, bot) 222 | if err != nil { 223 | logrus.Errorln(err) 224 | } 225 | }() 226 | } 227 | case update.InlineQuery != nil: 228 | updateQuery := *update.InlineQuery 229 | switch { 230 | case updateQuery.Query == "help": 231 | go func() { 232 | err = processInlineHelp(updateQuery, bot) 233 | if err != nil { 234 | logrus.Errorln(err) 235 | } 236 | }() 237 | case strings.Contains(updateQuery.Query, "search"): 238 | go func() { 239 | err = processInlineSearch(updateQuery, bot) 240 | if err != nil { 241 | logrus.Errorln(err) 242 | } 243 | }() 244 | default: 245 | go func() { 246 | musicID, _ := strconv.Atoi(linkTestMusic(updateQuery.Query)) 247 | if musicID != 0 { 248 | err = processInlineMusic(musicID, updateQuery, bot) 249 | if err != nil { 250 | logrus.Errorln(err) 251 | } 252 | } else { 253 | err = processEmptyInline(updateQuery, bot) 254 | if err != nil { 255 | logrus.Errorln(err) 256 | } 257 | } 258 | }() 259 | } 260 | } 261 | } 262 | return 0 263 | } 264 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/XiaoMengXinX/163KeyMarker v0.0.0-20221030134715-67afb724a936 h1:to5zIst65bM0aYtqsUU97yE6kuHKUB078F2z+G4ZQH4= 2 | github.com/XiaoMengXinX/163KeyMarker v0.0.0-20221030134715-67afb724a936/go.mod h1:L6oR/uZwUSBO7o57qbul/zwUuuKpP4EVnFKmTBscvLY= 3 | github.com/XiaoMengXinX/Music163Api-Go v0.1.30 h1:MqRItDFtX1J0JTlFtwN2RwjsYMA7/g/+cTjcOJXy19s= 4 | github.com/XiaoMengXinX/Music163Api-Go v0.1.30/go.mod h1:kLU/CkLxKnEJFCge0URvQ0lHt6ImoG1/2aVeNbgV2RQ= 5 | github.com/XiaoMengXinX/SimpleDownloader v0.0.0-20241104184306-5642193c58ed h1:zGY0v7IxjSTEMnnq/3MvZIRPdKc5p+VRkgXY7s2Bg5M= 6 | github.com/XiaoMengXinX/SimpleDownloader v0.0.0-20241104184306-5642193c58ed/go.mod h1:Fh8cPEMvudeU3D+sBG4FAoC4iOH8aGI7Z39oaN6/2iA= 7 | github.com/bogem/id3v2 v1.2.0 h1:hKDF+F1gOgQ5r1QmBCEZUk4MveJbKxCeIDSBU7CQ4oI= 8 | github.com/bogem/id3v2 v1.2.0/go.mod h1:t78PK5AQ56Q47kizpYiV6gtjj3jfxlz87oFpty8DYs8= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 13 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 14 | github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= 15 | github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= 16 | github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= 17 | github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= 18 | github.com/go-flac/flacpicture v0.3.0 h1:LkmTxzFLIynwfhHiZsX0s8xcr3/u33MzvV89u+zOT8I= 19 | github.com/go-flac/flacpicture v0.3.0/go.mod h1:DPbrzVYQ3fJcvSgLFp9HXIrEQEdfdk/+m0nQCzwodZI= 20 | github.com/go-flac/flacvorbis v0.2.0 h1:KH0xjpkNTXFER4cszH4zeJxYcrHbUobz/RticWGOESs= 21 | github.com/go-flac/flacvorbis v0.2.0/go.mod h1:uIysHOtuU7OLGoCRG92bvnkg7QEqHx19qKRV6K1pBrI= 22 | github.com/go-flac/go-flac v1.0.0 h1:6qI9XOVLcO50xpzm3nXvO31BgDgHhnr/p/rER/K/doY= 23 | github.com/go-flac/go-flac v1.0.0/go.mod h1:WnZhcpmq4u1UdZMNn9LYSoASpWOCMOoxXxcWEHSzkW8= 24 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= 25 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= 26 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= 27 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 28 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 29 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 30 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 31 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 32 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 33 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 34 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 35 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 36 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 37 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 38 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 39 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 43 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 44 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 45 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 46 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 47 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 48 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 49 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= 50 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= 51 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 52 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 53 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 54 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 55 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 58 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 59 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 60 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 61 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 62 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 63 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 64 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 66 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 67 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 68 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 69 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 70 | modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= 71 | modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= 72 | modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= 73 | modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= 74 | modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= 75 | modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 76 | modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= 77 | modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= 78 | modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE= 79 | modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0= 80 | modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= 81 | modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= 82 | modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= 83 | modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= 84 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 85 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 86 | modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= 87 | modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= 88 | modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= 89 | modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= 90 | modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= 91 | modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= 92 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 93 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 94 | -------------------------------------------------------------------------------- /bot/processMusic.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "path" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | marker "github.com/XiaoMengXinX/163KeyMarker" 14 | "github.com/XiaoMengXinX/Music163Api-Go/api" 15 | "github.com/XiaoMengXinX/Music163Api-Go/types" 16 | downloader "github.com/XiaoMengXinX/SimpleDownloader" 17 | "github.com/go-telegram-bot-api/telegram-bot-api/v5" 18 | "github.com/sirupsen/logrus" 19 | "gorm.io/gorm" 20 | ) 21 | 22 | // 限制并发任务数 23 | var musicLimiter = make(chan bool, 4) 24 | 25 | func processMusic(musicID int, message tgbotapi.Message, bot *tgbotapi.BotAPI) (err error) { 26 | d := downloader.NewDownloader().SetSavePath(cacheDir).SetBreakPoint(true) 27 | 28 | timeout, _ := strconv.Atoi(config["DownloadTimeout"]) 29 | if timeout != 0 { 30 | d.SetTimeOut(time.Duration(int64(timeout)) * time.Second) 31 | } else { 32 | d.SetTimeOut(60 * time.Second) // 默认超时时间为 60 秒 33 | } 34 | 35 | defer func() { 36 | e := recover() 37 | if e != nil { 38 | err = fmt.Errorf("%v", e) 39 | } 40 | }() 41 | var songInfo SongInfo 42 | var msgResult tgbotapi.Message 43 | 44 | sendFailed := func(err error) { 45 | var errText string 46 | if strings.Contains(fmt.Sprintf("%v", err), md5VerFailed) || strings.Contains(fmt.Sprintf("%v", err), downloadTimeout) { 47 | errText = "%v" 48 | } else { 49 | errText = uploadFailed 50 | } 51 | editMsg := tgbotapi.NewEditMessageText(message.Chat.ID, msgResult.MessageID, fmt.Sprintf(musicInfoMsg+errText, songInfo.SongName, songInfo.SongAlbum, songInfo.FileExt, float64(songInfo.MusicSize)/1024/1024, strings.ReplaceAll(err.Error(), config["BOT_TOKEN"], "BOT_TOKEN"))) 52 | _, err = bot.Send(editMsg) 53 | if err != nil { 54 | logrus.Errorln(err) 55 | } 56 | } 57 | 58 | db := MusicDB.Session(&gorm.Session{}) 59 | err = db.Where("music_id = ?", musicID).First(&songInfo).Error 60 | if err == nil { 61 | msg := tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf(musicInfoMsg+hitCache, songInfo.SongName, songInfo.SongAlbum, songInfo.FileExt, float64(songInfo.MusicSize)/1024/1024)) 62 | msg.ReplyToMessageID = message.MessageID 63 | msgResult, err = bot.Send(msg) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | _, err := sendMusic(songInfo, "", "", message, bot) 69 | if err != nil { 70 | sendFailed(err) 71 | return err 72 | } 73 | 74 | deleteMsg := tgbotapi.NewDeleteMessage(msgResult.Chat.ID, msgResult.MessageID) 75 | _, err = bot.Request(deleteMsg) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return err 81 | } 82 | msg := tgbotapi.NewMessage(message.Chat.ID, waitForDown) 83 | msg.ReplyToMessageID = message.MessageID 84 | msgResult, err = bot.Send(msg) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | musicLimiter <- true 90 | defer func() { 91 | <-musicLimiter 92 | }() 93 | 94 | err = db.Where("music_id = ?", musicID).First(&songInfo).Error 95 | if err == nil { 96 | editMsg := tgbotapi.NewEditMessageText(message.Chat.ID, msgResult.MessageID, fmt.Sprintf(musicInfoMsg+hitCache, songInfo.SongName, songInfo.SongAlbum, songInfo.FileExt, float64(songInfo.MusicSize)/1024/1024)) 97 | msgResult, err = bot.Send(editMsg) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | _, err := sendMusic(songInfo, "", "", message, bot) 103 | if err != nil { 104 | sendFailed(err) 105 | return err 106 | } 107 | 108 | deleteMsg := tgbotapi.NewDeleteMessage(msgResult.Chat.ID, msgResult.MessageID) 109 | _, err = bot.Request(deleteMsg) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | return err 115 | } 116 | 117 | editMsg := tgbotapi.NewEditMessageText(message.Chat.ID, msgResult.MessageID, fetchInfo) 118 | _, err = bot.Send(editMsg) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | b := api.NewBatch( 124 | api.BatchAPI{ 125 | Key: api.SongDetailAPI, 126 | Json: api.CreateSongDetailReqJson([]int{musicID}), 127 | }, 128 | api.BatchAPI{ 129 | Key: api.SongUrlAPI, 130 | Json: api.CreateSongURLJson(api.SongURLConfig{Ids: []int{musicID}}), 131 | }, 132 | ) 133 | if b.Do(data).Error != nil { 134 | return err 135 | } 136 | _, result := b.Parse() 137 | 138 | var songDetail types.SongsDetailData 139 | _ = json.Unmarshal([]byte(result[api.SongDetailAPI]), &songDetail) 140 | 141 | var songURL types.SongsURLData 142 | _ = json.Unmarshal([]byte(result[api.SongUrlAPI]), &songURL) 143 | 144 | if len(songDetail.Songs) == 0 || len(songURL.Data) == 0 { 145 | editMsg := tgbotapi.NewEditMessageText(message.Chat.ID, msgResult.MessageID, fetchInfoFailed) 146 | _, err = bot.Send(editMsg) 147 | if err != nil { 148 | logrus.Errorln(err) 149 | } 150 | return err 151 | } 152 | if songURL.Data[0].Url == "" { 153 | editMsg := tgbotapi.NewEditMessageText(message.Chat.ID, msgResult.MessageID, getUrlFailed) 154 | _, err = bot.Send(editMsg) 155 | if err != nil { 156 | logrus.Errorln(err) 157 | } 158 | return err 159 | } 160 | 161 | songInfo.FromChatID = message.Chat.ID 162 | if message.Chat.IsPrivate() { 163 | songInfo.FromChatName = message.Chat.UserName 164 | } else { 165 | songInfo.FromChatName = message.Chat.Title 166 | } 167 | songInfo.FromUserID = message.From.ID 168 | songInfo.FromUserName = message.From.UserName 169 | 170 | songInfo.MusicID = musicID 171 | songInfo.Duration = songDetail.Songs[0].Dt / 1000 172 | songInfo.SongName = songDetail.Songs[0].Name // 解析歌曲信息 173 | songInfo.SongArtists = parseArtist(songDetail.Songs[0]) 174 | songInfo.SongAlbum = songDetail.Songs[0].Al.Name 175 | url := songURL.Data[0].Url 176 | // 从 URL 中移除查询参数以正确获取扩展名 177 | baseURL := url 178 | if queryIndex := strings.Index(url, "?"); queryIndex != -1 { 179 | baseURL = url[:queryIndex] 180 | } 181 | switch path.Ext(path.Base(baseURL)) { 182 | case ".mp3": 183 | songInfo.FileExt = "mp3" 184 | case ".flac": 185 | songInfo.FileExt = "flac" 186 | default: 187 | songInfo.FileExt = "mp3" 188 | } 189 | songInfo.MusicSize = songURL.Data[0].Size 190 | songInfo.BitRate = 8 * songURL.Data[0].Size / (songDetail.Songs[0].Dt / 1000) 191 | 192 | if picRes, err := http.Head(songDetail.Songs[0].Al.PicUrl); err == nil { 193 | songInfo.PicSize = int(picRes.ContentLength) 194 | } else { 195 | logrus.Errorln(err) 196 | } 197 | 198 | editMsg = tgbotapi.NewEditMessageText(message.Chat.ID, msgResult.MessageID, fmt.Sprintf(musicInfoMsg+downloading, songInfo.SongName, songInfo.SongAlbum, songInfo.FileExt, float64(songInfo.MusicSize)/1024/1024)) 199 | _, err = bot.Send(editMsg) 200 | if err != nil { 201 | return err 202 | } 203 | 204 | hostReplacer := strings.NewReplacer("m8.", "m7.", "m801.", "m701.", "m804.", "m701.", "m704.", "m701.") 205 | 206 | timeStamp := time.Now().UnixMicro() 207 | musicFileName := fmt.Sprintf("%d-%s", timeStamp, path.Base(url)) 208 | 209 | task, _ := d.NewDownloadTask(url) 210 | host := task.GetHostName() 211 | task.ReplaceHostName(hostReplacer.Replace(host)).ForceHttps().ForceMultiThread() 212 | errCh := task.SetFileName(musicFileName).DownloadWithChannel() 213 | 214 | updateStatus := func(task *downloader.DownloadTask, ch chan error, statusText string) (err error) { 215 | var lastUpdateTime int64 216 | loop: 217 | for { 218 | select { 219 | case err = <-ch: 220 | break loop 221 | default: 222 | writtenBytes := task.GetWrittenBytes() 223 | if task.GetFileSize() == 0 || writtenBytes == 0 || time.Now().Unix()-lastUpdateTime < 5 { 224 | continue 225 | } 226 | editMsg = tgbotapi.NewEditMessageText(message.Chat.ID, msgResult.MessageID, fmt.Sprintf(musicInfoMsg+statusText+downloadStatus, songInfo.SongName, songInfo.SongAlbum, songInfo.FileExt, float64(songInfo.MusicSize)/1024/1024, task.CalculateSpeed(time.Millisecond*500), float64(writtenBytes)/1024/1024, float64(task.GetFileSize())/1024/1024, (writtenBytes*100)/task.GetFileSize())) 227 | _, _ = bot.Send(editMsg) 228 | lastUpdateTime = time.Now().Unix() 229 | } 230 | } 231 | return err 232 | } 233 | 234 | err = updateStatus(task, errCh, downloading) 235 | if err != nil { 236 | if config["ReverseProxy"] != "" { 237 | ch := task.WithResolvedIpOnHost(config["ReverseProxy"]).DownloadWithChannel() 238 | err = updateStatus(task, ch, redownloading) 239 | if err != nil { 240 | sendFailed(err) 241 | task.CleanTempFiles() 242 | return err 243 | } 244 | } else { 245 | sendFailed(err) 246 | task.CleanTempFiles() 247 | return err 248 | } 249 | } 250 | 251 | isMD5Verified, _ := verifyMD5(cacheDir+"/"+musicFileName, songURL.Data[0].Md5) 252 | if !isMD5Verified && songURL.Data[0].Md5 != "" { 253 | err = os.Remove(cacheDir + "/" + fmt.Sprintf("%d-%s", timeStamp, path.Base(url))) 254 | if err != nil { 255 | logrus.Errorln(err) 256 | } 257 | sendFailed(fmt.Errorf("%s\n%s", md5VerFailed, retryLater)) 258 | return nil 259 | } 260 | 261 | var picPath, resizePicPath string 262 | p, _ := d.NewDownloadTask(songDetail.Songs[0].Al.PicUrl) 263 | err = p.SetFileName(fmt.Sprintf("%d-%s", timeStamp, path.Base(songDetail.Songs[0].Al.PicUrl))).Download() 264 | if err != nil { 265 | logrus.Errorln(err) 266 | } else { 267 | picPath = cacheDir + "/" + fmt.Sprintf("%d-%s", timeStamp, path.Base(songDetail.Songs[0].Al.PicUrl)) 268 | var err error 269 | resizePicPath, err = resizeImg(picPath) 270 | if err != nil { 271 | logrus.Errorln(err) 272 | } 273 | } 274 | 275 | var musicPic string 276 | picStat, err := os.Stat(picPath) 277 | if picStat != nil && err == nil { 278 | if picStat.Size() > 2*1024*1024 { 279 | musicPic = resizePicPath 280 | embPicStat, _ := os.Stat(resizePicPath) 281 | songInfo.EmbPicSize = int(embPicStat.Size()) 282 | } else { 283 | musicPic = picPath 284 | songInfo.EmbPicSize = songInfo.PicSize 285 | } 286 | } else { 287 | logrus.Errorln(err) 288 | } 289 | 290 | var pic *os.File = nil 291 | 292 | if picStat != nil && err == nil { 293 | pic, _ = os.Open(musicPic) 294 | defer pic.Close() 295 | } 296 | 297 | var replacer = strings.NewReplacer("/", " ", "?", " ", "*", " ", ":", " ", "|", " ", "\\", " ", "<", " ", ">", " ", "\"", " ") 298 | var newDir = cacheDir+"/"+fmt.Sprintf("%d", timeStamp) 299 | fileName := replacer.Replace(fmt.Sprintf("%v - %v.%v", strings.Replace(songInfo.SongArtists, "/", ",", -1), songInfo.SongName, songInfo.FileExt)) 300 | var filePath = newDir+"/"+fileName 301 | err = os.Mkdir(newDir, os.ModePerm) 302 | if err != nil { 303 | sendFailed(err) 304 | return err 305 | } 306 | err = os.Rename(cacheDir+"/"+fmt.Sprintf("%d-%s", timeStamp, path.Base(url)), filePath) 307 | if err != nil { 308 | filePath = cacheDir+"/"+fmt.Sprintf("%d-%s", timeStamp, path.Base(url)) 309 | } 310 | 311 | mark := marker.CreateMarker(songDetail.Songs[0], songURL.Data[0]) 312 | 313 | file, _ := os.Open(filePath) 314 | defer file.Close() 315 | 316 | err = marker.AddMusicID3V2(file, pic, mark) 317 | if err != nil { 318 | file, _ = os.Open(filePath) 319 | defer file.Close() 320 | err = marker.AddMusicID3V2(file, nil, mark) 321 | } 322 | if err != nil { 323 | sendFailed(err) 324 | return err 325 | } 326 | 327 | editMsg = tgbotapi.NewEditMessageText(message.Chat.ID, msgResult.MessageID, fmt.Sprintf(musicInfoMsg+uploading, songInfo.SongName, songInfo.SongAlbum, songInfo.FileExt, float64(songInfo.MusicSize)/1024/1024)) 328 | _, err = bot.Send(editMsg) 329 | if err != nil { 330 | return err 331 | } 332 | 333 | audio, err := sendMusic(songInfo, filePath, resizePicPath, message, bot) 334 | if err != nil { 335 | sendFailed(err) 336 | return err 337 | } 338 | 339 | songInfo.FileID = audio.Audio.FileID 340 | if audio.Audio.Thumbnail != nil { 341 | songInfo.ThumbFileID = audio.Audio.Thumbnail.FileID 342 | } 343 | 344 | err = db.Create(&songInfo).Error // 写入歌曲缓存 345 | if err != nil { 346 | return err 347 | } 348 | 349 | for _, f := range []string{filePath, resizePicPath, picPath} { 350 | err := os.Remove(f) 351 | if err != nil { 352 | logrus.Errorln(err) 353 | } 354 | } 355 | err = os.RemoveAll(newDir) 356 | if err != nil { 357 | logrus.Errorln(err) 358 | } 359 | 360 | deleteMsg := tgbotapi.NewDeleteMessage(msgResult.Chat.ID, msgResult.MessageID) 361 | _, err = bot.Request(deleteMsg) 362 | if err != nil { 363 | return err 364 | } 365 | 366 | return err 367 | } 368 | 369 | func sendMusic(songInfo SongInfo, musicPath, picPath string, message tgbotapi.Message, bot *tgbotapi.BotAPI) (audio tgbotapi.Message, err error) { 370 | var numericKeyboard tgbotapi.InlineKeyboardMarkup 371 | numericKeyboard = tgbotapi.NewInlineKeyboardMarkup( 372 | tgbotapi.NewInlineKeyboardRow( 373 | tgbotapi.NewInlineKeyboardButtonURL(fmt.Sprintf("%s- %s", songInfo.SongName, songInfo.SongArtists), fmt.Sprintf("https://music.163.com/song?id=%d", songInfo.MusicID)), 374 | ), 375 | tgbotapi.NewInlineKeyboardRow( 376 | tgbotapi.NewInlineKeyboardButtonSwitch(sendMeTo, fmt.Sprintf("https://music.163.com/song?id=%d", songInfo.MusicID)), 377 | ), 378 | ) 379 | var newAudio tgbotapi.AudioConfig 380 | if songInfo.FileID != "" { 381 | newAudio = tgbotapi.NewAudio(message.Chat.ID, tgbotapi.FileID(songInfo.FileID)) 382 | } else { 383 | newAudio = tgbotapi.NewAudio(message.Chat.ID, tgbotapi.FilePath(musicPath)) 384 | status := tgbotapi.NewChatAction(message.Chat.ID, "upload_document") 385 | _, _ = bot.Send(status) 386 | } 387 | newAudio.Caption = fmt.Sprintf(musicInfo, songInfo.SongName, songInfo.SongArtists, songInfo.SongAlbum, songInfo.FileExt, float64(songInfo.MusicSize+songInfo.EmbPicSize)/1024/1024, float64(songInfo.BitRate)/1000, botName) 388 | newAudio.Title = fmt.Sprintf("%s", songInfo.SongName) 389 | newAudio.Performer = songInfo.SongArtists 390 | newAudio.Duration = songInfo.Duration 391 | newAudio.ReplyMarkup = numericKeyboard 392 | newAudio.ReplyToMessageID = message.MessageID 393 | if songInfo.ThumbFileID != "" { 394 | newAudio.Thumb = tgbotapi.FileID(songInfo.ThumbFileID) 395 | } 396 | if picPath != "" { 397 | newAudio.Thumb = tgbotapi.FilePath(picPath) 398 | } 399 | audio, err = bot.Send(newAudio) 400 | if strings.Contains(fmt.Sprintf("%v", err), "replied message not found") { 401 | newAudio.ReplyToMessageID = 0 402 | audio, err = bot.Send(newAudio) 403 | } 404 | return audio, err 405 | } 406 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------