initial commit
This commit is contained in:
commit
43107bf5c1
12 changed files with 667 additions and 0 deletions
92
.gitignore
vendored
Normal file
92
.gitignore
vendored
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
### JetBrains template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### Go template
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
*.bat
|
||||||
|
/temp
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
20
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
20
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="GoUnhandledErrorResult" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<methods>
|
||||||
|
<method importPath="hash" receiver="Hash" name="Write" />
|
||||||
|
<method importPath="strings" receiver="*Builder" name="Write" />
|
||||||
|
<method importPath="strings" receiver="*Builder" name="WriteByte" />
|
||||||
|
<method importPath="bytes" receiver="*Buffer" name="WriteRune" />
|
||||||
|
<method importPath="bytes" receiver="*Buffer" name="Write" />
|
||||||
|
<method importPath="bytes" receiver="*Buffer" name="WriteString" />
|
||||||
|
<method importPath="strings" receiver="*Builder" name="WriteString" />
|
||||||
|
<method importPath="bytes" receiver="*Buffer" name="WriteByte" />
|
||||||
|
<method importPath="strings" receiver="*Builder" name="WriteRune" />
|
||||||
|
<method importPath="math/rand" receiver="*Rand" name="Read" />
|
||||||
|
<method importPath="io" receiver="ReadCloser" name="Close" />
|
||||||
|
</methods>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
11
.idea/lrc2srt.iml
generated
Normal file
11
.idea/lrc2srt.iml
generated
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/lrc2srt.iml" filepath="$PROJECT_DIR$/.idea/lrc2srt.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
60
cloudlyric.go
Normal file
60
cloudlyric.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloudLyricBase struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
Lyric string `json:"lyric"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloudLyric struct {
|
||||||
|
Sgc bool `json:"sgc"`
|
||||||
|
Sfy bool `json:"sfy"`
|
||||||
|
Qfy bool `json:"qfy"`
|
||||||
|
TransUser interface{} `json:"transUser"`
|
||||||
|
Lrc CloudLyricBase `json:"lrc"`
|
||||||
|
TLyric CloudLyricBase `json:"tlyric"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get163Lyric(id string) (lyric, tLyric string) {
|
||||||
|
api := "https://music.163.com/api/song/lyric"
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("os", "pc")
|
||||||
|
//歌曲的id号
|
||||||
|
params.Add("id", id)
|
||||||
|
//包含原始歌词
|
||||||
|
params.Add("lv", "1")
|
||||||
|
//包含翻译歌词
|
||||||
|
params.Add("tv", "1")
|
||||||
|
api = fmt.Sprintf("%s?%s", api, params.Encode())
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", api, nil)
|
||||||
|
//必须设置Referer,否则会请求失败
|
||||||
|
req.Header.Add("Referer", "https://music.163.com")
|
||||||
|
req.Header.Add("User-Agent", ChromeUA)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("网络错误:%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if resp == nil || resp.StatusCode != http.StatusOK {
|
||||||
|
fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var cloudLyric CloudLyric
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&cloudLyric)
|
||||||
|
if cloudLyric.Sgc {
|
||||||
|
fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", cloudLyric)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return cloudLyric.Lrc.Lyric, cloudLyric.TLyric.Lyric
|
||||||
|
}
|
7
go.mod
Normal file
7
go.mod
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module github.com/hami_lemon/lrc2srt
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require github.com/jessevdk/go-flags v1.5.0
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect
|
4
go.sum
Normal file
4
go.sum
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
329
lts.go
Normal file
329
lts.go
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SRTContent struct {
|
||||||
|
//序号,从1开始
|
||||||
|
Index int
|
||||||
|
//开始时间,单位毫秒
|
||||||
|
Start int
|
||||||
|
//结束时间,单位毫秒
|
||||||
|
End int
|
||||||
|
//歌词内容
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
type SRT struct {
|
||||||
|
//歌曲名
|
||||||
|
Title string
|
||||||
|
//歌手名 未指定文件名是,文件名格式为:歌曲名-歌手名.srt
|
||||||
|
Artist string
|
||||||
|
Content []*SRTContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option 运行时传入的选项
|
||||||
|
type Option struct {
|
||||||
|
Id string `short:"i" long:"id" description:"歌曲的id,网易云和QQ音乐均可。"`
|
||||||
|
Input string `short:"I" long:"input" description:"需要转换的LRC文件路径。"`
|
||||||
|
Source string `short:"s" long:"source" description:"当设置id时有效,指定从网易云(163)还是QQ音乐(qq)上获取歌词。" default:"163" choice:"163" choice:"qq" choice:"QQ"`
|
||||||
|
Download bool `short:"d" long:"download" description:"只下载歌词,而不进行解析。"`
|
||||||
|
Mode int `short:"m" long:"mode" default:"1" description:"原文和译文的排列模式,可选值有:[1] [2] [3]" choice:"1" choice:"2" choice:"3"`
|
||||||
|
Version bool `short:"v" long:"version" description:"获取版本信息"`
|
||||||
|
Output string `no-flag:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// VERSION 当前版本
|
||||||
|
VERSION = `"0.1.0" (build 2022.02.18)`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36"
|
||||||
|
client = http.Client{}
|
||||||
|
opt Option
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args, err := flags.Parse(&opt)
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
//显示版本信息
|
||||||
|
if opt.Version {
|
||||||
|
fmt.Printf("LrcToSrt(lts) version %s\n", VERSION)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
//获取保存的文件名
|
||||||
|
if len(args) != 0 {
|
||||||
|
opt.Output = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取歌词
|
||||||
|
var lyric, tranLyric string
|
||||||
|
if opt.Id != "" {
|
||||||
|
if opt.Source != "163" {
|
||||||
|
lyric, tranLyric = GetQQLyric(opt.Id)
|
||||||
|
} else {
|
||||||
|
lyric, tranLyric = Get163Lyric(opt.Id)
|
||||||
|
}
|
||||||
|
//下载歌词
|
||||||
|
if opt.Download {
|
||||||
|
//对文件名进行处理
|
||||||
|
o := opt.Output
|
||||||
|
if o == "" {
|
||||||
|
o = opt.Id + ".lrc"
|
||||||
|
} else if !strings.HasSuffix(o, ".lrc") {
|
||||||
|
o += ".lrc"
|
||||||
|
}
|
||||||
|
WriteFile(o, lyric)
|
||||||
|
if tranLyric != "" {
|
||||||
|
WriteFile("tran_"+o, tranLyric)
|
||||||
|
}
|
||||||
|
fmt.Println("下载歌词完成!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if opt.Input != "" {
|
||||||
|
//从文件中获取歌词
|
||||||
|
if !strings.HasSuffix(opt.Input, ".lrc") {
|
||||||
|
fmt.Println("Error: 不支持的格式,目前只支持lrc歌词文件。")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
lyric = ReadFile(opt.Input)
|
||||||
|
if lyric == "" {
|
||||||
|
fmt.Println("获取歌词失败,文件内容为空。")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Error: 请指定需要转换的歌词。")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
lyricSRT, tranLyricSRT := Lrc2Srt(lyric), Lrc2Srt(tranLyric)
|
||||||
|
SaveSRT(lyricSRT, tranLyricSRT, opt.Output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveSRT 保存数据为SRT文件
|
||||||
|
func SaveSRT(srt *SRT, tranSrt *SRT, name string) {
|
||||||
|
if tranSrt == nil {
|
||||||
|
//没有译文时,用一个空的对象的代替,减少nil判断
|
||||||
|
tranSrt = &SRT{Content: make([]*SRTContent, 0)}
|
||||||
|
}
|
||||||
|
//处理结果文件的文件名
|
||||||
|
if name == "" {
|
||||||
|
title := srt.Title
|
||||||
|
if title != "" {
|
||||||
|
//以歌曲名命名
|
||||||
|
name = fmt.Sprintf("%s.srt", title)
|
||||||
|
} else if opt.Id != "" {
|
||||||
|
//以歌曲的id命名
|
||||||
|
name = fmt.Sprintf("%s.srt", opt.Id)
|
||||||
|
} else if opt.Input != "" {
|
||||||
|
//以LRC文件的文件名命名
|
||||||
|
name = fmt.Sprintf("%s.srt", opt.Input[:len(opt.Input)-4])
|
||||||
|
} else {
|
||||||
|
//以当前时间的毫秒值命名
|
||||||
|
name = fmt.Sprintf("%d.srt", time.Now().Unix())
|
||||||
|
}
|
||||||
|
} else if !strings.HasSuffix(name, ".srt") {
|
||||||
|
name += ".srt"
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("保存结果失败:%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
writer := bufio.NewWriter(file)
|
||||||
|
|
||||||
|
index := 1
|
||||||
|
switch opt.Mode {
|
||||||
|
case 1:
|
||||||
|
//译文和原文交错排列
|
||||||
|
_len, _lent, size := len(srt.Content), len(tranSrt.Content), 0
|
||||||
|
if _len > _lent {
|
||||||
|
size = 2 * _len
|
||||||
|
} else {
|
||||||
|
size = 2 * _lent
|
||||||
|
}
|
||||||
|
//临时缓冲区,偶数索引存原文,奇数存译文
|
||||||
|
buf := make([]*SRTContent, size, size)
|
||||||
|
for i, item := range srt.Content {
|
||||||
|
buf[2*i] = item
|
||||||
|
}
|
||||||
|
for i, item := range tranSrt.Content {
|
||||||
|
buf[2*i+1] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
//写入文件
|
||||||
|
for _, item := range buf {
|
||||||
|
if item != nil {
|
||||||
|
item.Index = index
|
||||||
|
_, _ = writer.WriteString(item.String())
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
//原文在上,译文在下
|
||||||
|
for _, item := range srt.Content {
|
||||||
|
item.Index = index
|
||||||
|
_, _ = writer.WriteString(item.String())
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range tranSrt.Content {
|
||||||
|
item.Index = index
|
||||||
|
_, _ = writer.WriteString(item.String())
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
//译文在上,原文在下
|
||||||
|
for _, item := range tranSrt.Content {
|
||||||
|
item.Index = index
|
||||||
|
_, _ = writer.WriteString(item.String())
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
for _, item := range srt.Content {
|
||||||
|
item.Index = index
|
||||||
|
_, _ = writer.WriteString(item.String())
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = writer.Flush()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("保存结果失败:%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("转换文件完成,保存结果为:%s\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lrc2Srt 将原始个LRC字符串歌词解析SRT对象
|
||||||
|
func Lrc2Srt(src string) *SRT {
|
||||||
|
if src == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
//标准的LRC文件为一行一句歌词
|
||||||
|
lyrics := strings.Split(src, "\n")
|
||||||
|
//标识标签的正则 [ar:A-SOUL]形式
|
||||||
|
infoRegx := regexp.MustCompile(`^\[([a-z]+):([\s\S]*)]`)
|
||||||
|
|
||||||
|
srt := &SRT{Content: make([]*SRTContent, 0, len(lyrics))}
|
||||||
|
//解析标识信息
|
||||||
|
for {
|
||||||
|
if len(lyrics) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
l := lyrics[0]
|
||||||
|
//根据正则表达式进行匹配
|
||||||
|
info := infoRegx.FindStringSubmatch(l)
|
||||||
|
//标识信息位于歌词信息前面,当出现未匹配成功时,即可退出循环
|
||||||
|
if info != nil {
|
||||||
|
//info 中为匹配成功的字符串和 子组合(正则表达式中括号包裹的部分)
|
||||||
|
//例如,对于标识信息:[ar:A-SOUL],info中的数据为[[ar:A-SOUL] ar A-SOUL]
|
||||||
|
key := info[1]
|
||||||
|
switch key {
|
||||||
|
case "ar":
|
||||||
|
//歌手名
|
||||||
|
if len(info) == 3 {
|
||||||
|
srt.Artist = info[2]
|
||||||
|
}
|
||||||
|
case "ti":
|
||||||
|
//歌曲名
|
||||||
|
if len(info) == 3 {
|
||||||
|
srt.Title = info[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lyrics = lyrics[1:]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//歌词信息的正则,"[00:10.222]超级的敏感"或“[00:10:222]超级的敏感”或“[00:10]超级的敏感”或“[00:10.22]超级的敏感”或“[00:10:22]超级的敏感”
|
||||||
|
lyricRegx := regexp.MustCompile(`\[(\d\d):(\d\d)([.:]\d{2,3})?]([\s\S]+)`)
|
||||||
|
index := 0
|
||||||
|
for _, l := range lyrics {
|
||||||
|
content := lyricRegx.FindStringSubmatch(l)
|
||||||
|
if content != nil {
|
||||||
|
c := SplitLyric(content[1:])
|
||||||
|
if c != nil {
|
||||||
|
if index != 0 {
|
||||||
|
//前一条字幕的结束时间为当前字幕开始的时间
|
||||||
|
srt.Content[index-1].End = c.Start
|
||||||
|
}
|
||||||
|
srt.Content = append(srt.Content, c)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//最后一条字幕
|
||||||
|
last := srt.Content[index-1]
|
||||||
|
//最后一条字幕的结束时间为其开始时间 + 10秒
|
||||||
|
last.End = last.Start + 10000
|
||||||
|
return srt
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitLyric 对分割出来的歌词信息进行解析
|
||||||
|
func SplitLyric(src []string) *SRTContent {
|
||||||
|
minute, err := strconv.Atoi(src[0])
|
||||||
|
second, err := strconv.Atoi(src[1])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("错误的时间格式:%s\n", src)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
millisecond, content := 0, ""
|
||||||
|
|
||||||
|
_len := len(src)
|
||||||
|
if _len == 3 {
|
||||||
|
//歌词信息没有毫秒值
|
||||||
|
content = src[2]
|
||||||
|
} else if _len == 4 {
|
||||||
|
content = src[3]
|
||||||
|
//字符串的第一个字符是 "." 或 ":"
|
||||||
|
ms := src[2][1:]
|
||||||
|
millisecond, err = strconv.Atoi(ms)
|
||||||
|
//QQ音乐歌词文件中,毫秒值只有两位,需要特殊处理一下
|
||||||
|
if len(ms) == 2 {
|
||||||
|
millisecond *= 10
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("错误的时间格式:%s\n", src)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
srtContent := &SRTContent{}
|
||||||
|
srtContent.Start = Time2Millisecond(minute, second, millisecond)
|
||||||
|
srtContent.Text = content
|
||||||
|
return srtContent
|
||||||
|
}
|
||||||
|
|
||||||
|
//返回SRT文件中,一句字幕的字符串表示形式
|
||||||
|
/**
|
||||||
|
1
|
||||||
|
00:00:01,111 --> 00:00:10,111
|
||||||
|
字幕
|
||||||
|
|
||||||
|
*/
|
||||||
|
func (s *SRTContent) String() string {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
builder.WriteString(strconv.Itoa(s.Index))
|
||||||
|
builder.WriteByte('\n')
|
||||||
|
sh, sm, ss, sms := Millisecond2Time(s.Start)
|
||||||
|
eh, em, es, ems := Millisecond2Time(s.End)
|
||||||
|
builder.WriteString(fmt.Sprintf("%02d:%02d:%02d,%03d --> %02d:%02d:%02d,%03d\n",
|
||||||
|
sh, sm, ss, sms, eh, em, es, ems))
|
||||||
|
builder.WriteString(s.Text)
|
||||||
|
builder.WriteString("\n\n")
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
62
qqlyric.go
Normal file
62
qqlyric.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QQLyric struct {
|
||||||
|
RetCode int `json:"retcode"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
SubCode int `json:"subcode"`
|
||||||
|
Lyric string `json:"lyric"`
|
||||||
|
Trans string `json:"trans"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQQLyric(id string) (lyric, tLyric string) {
|
||||||
|
api := "https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg"
|
||||||
|
params := url.Values{}
|
||||||
|
//返回格式
|
||||||
|
params.Add("format", "json")
|
||||||
|
params.Add("inCharset", "utf-8")
|
||||||
|
params.Add("outCharset", "utf-8")
|
||||||
|
params.Add("platform", "yqq.json")
|
||||||
|
params.Add("g_tk", "5381")
|
||||||
|
//歌曲的id号
|
||||||
|
params.Add("songmid", id)
|
||||||
|
//返回结果为原始结果,而不是base64编码的结果(base64编码后数据量会增大)
|
||||||
|
params.Add("nobase64", "1")
|
||||||
|
|
||||||
|
api = fmt.Sprintf("%s?%s", api, params.Encode())
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", api, nil)
|
||||||
|
//必须设置Referer,否则会请求失败
|
||||||
|
req.Header.Add("Referer", "https://y.qq.com")
|
||||||
|
req.Header.Add("User-Agent", ChromeUA)
|
||||||
|
req.Header.Add("accept-encoding", "gzip")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("网络错误:%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp == nil || resp.StatusCode != http.StatusOK {
|
||||||
|
fmt.Printf("网络请求失败,状态码为:%d\n", resp.StatusCode)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
//返回的数据是gzip压缩,需要解压
|
||||||
|
reader, _ := gzip.NewReader(resp.Body)
|
||||||
|
var qqLyric QQLyric
|
||||||
|
err = json.NewDecoder(reader).Decode(&qqLyric)
|
||||||
|
if qqLyric.RetCode != 0 {
|
||||||
|
fmt.Printf("获取歌词失败,返回的结果为:%+v,请检查id是否正确\n", qqLyric)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return qqLyric.Lyric, qqLyric.Trans
|
||||||
|
}
|
60
util.go
Normal file
60
util.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Time2Millisecond 根据分,秒,毫秒 计算出对应的毫秒值
|
||||||
|
func Time2Millisecond(m, s, ms int) int {
|
||||||
|
t := m*60 + s
|
||||||
|
t *= 1000
|
||||||
|
t += ms
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Millisecond2Time 根据毫秒值计算出对应的 时,分,秒,毫秒形式的时间值
|
||||||
|
func Millisecond2Time(millisecond int) (h, m, s, ms int) {
|
||||||
|
ms = millisecond % 1000
|
||||||
|
|
||||||
|
s = millisecond / 1000
|
||||||
|
m = s / 60
|
||||||
|
h = m / 60
|
||||||
|
|
||||||
|
s %= 60
|
||||||
|
m %= 60
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadFile(name string) string {
|
||||||
|
if name == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
file, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("打开文件失败:%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("读取文件失败:%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteFile(name string, data string) {
|
||||||
|
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("保存文件失败:%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
nw, err := file.WriteString(data)
|
||||||
|
if err != nil || nw < len(data) {
|
||||||
|
fmt.Printf("保存文件失败:%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
Reference in a new issue