finish qq provider

fix session cmd: save session every in every command

add qq init

fix init sequence

update gitignore

fix miaosic cmd

add kugou album info in search

update qq music api.
This commit is contained in:
aynakeya
2025-07-06 23:50:19 +08:00
parent d2d10c3855
commit 971f5fc4e5
26 changed files with 2003 additions and 4 deletions

View File

@@ -0,0 +1,40 @@
package qq
import "strings"
type Credential struct {
OpenID string `json:"openid"`
RefreshToken string `json:"refresh_token"`
AccessToken string `json:"access_token"`
ExpiredAt int64 `json:"expired_at"`
MusicID int64 `json:"musicid"`
MusicKey string `json:"musickey"`
UnionID string `json:"unionid"`
StrMusicID string `json:"str_musicid"`
RefreshKey string `json:"refresh_key"`
EncryptUin string `json:"encryptUin"`
LoginType int `json:"loginType"`
}
func NewCredential() *Credential {
return &Credential{}
}
func (c *Credential) GetFormatedLoginType() int {
if c.LoginType == 0 {
if c.MusicKey != "" && strings.HasPrefix(c.MusicKey, "W_X") {
c.LoginType = 1
} else {
c.LoginType = 2
}
}
return c.LoginType
}
func (c *Credential) HasMusicID() bool {
return c.MusicID != 0
}
func (c *Credential) HasMusicKey() bool {
return c.MusicKey != ""
}

141
providers/qq/device.go Normal file
View File

@@ -0,0 +1,141 @@
package qq
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/google/uuid"
"strings"
)
// OSVersion 系统版本信息
type OSVersion struct {
Incremental string `json:"incremental"`
Release string `json:"release"`
Codename string `json:"codename"`
Sdk int `json:"sdk"`
}
func newOSVersion() *OSVersion {
return &OSVersion{
Incremental: "5891938",
Release: "10",
Codename: "REL",
Sdk: 29,
}
}
// Device 设备相关信息
type Device struct {
Display string `json:"display"`
Product string `json:"product"`
Device string `json:"device"`
Board string `json:"board"`
Model string `json:"model"`
Fingerprint string `json:"fingerprint"`
BootID string `json:"boot_id"`
ProcVersion string `json:"proc_version"`
IMEI string `json:"imei"`
Brand string `json:"brand"`
Bootloader string `json:"bootloader"`
BaseBand string `json:"base_band"`
Version *OSVersion `json:"version"`
SimInfo string `json:"sim_info"`
OsType string `json:"os_type"`
MacAddress string `json:"mac_address"`
WifiBSSID string `json:"wifi_bssid"`
WifiSSID string `json:"wifi_ssid"`
IMSIMD5 []byte `json:"imsi_md5"`
AndroidID string `json:"android_id"`
APN string `json:"apn"`
VendorName string `json:"vendor_name"`
VendorOSName string `json:"vendor_os_name"`
Qimei string `json:"qimei,omitempty"`
}
// 生成随机IMEI号码
func deviceGetRandomIMEI() string {
imei := make([]int, 14)
sum := 0
for i := range imei {
num := rng.Intn(10)
if (i+2)%2 == 0 {
num *= 2
if num >= 10 {
num = (num % 10) + 1
}
}
sum += num
imei[i] = num
}
ctrlDigit := (sum * 9) % 10
return fmt.Sprintf("%s%d", intSliceToString(imei), ctrlDigit)
}
func deviceGetRandomHex(length int) string {
bytes := make([]byte, length)
_, _ = rng.Read(bytes)
return hex.EncodeToString(bytes)
}
func intSliceToString(slice []int) string {
var builder strings.Builder
for _, num := range slice {
builder.WriteString(fmt.Sprintf("%d", num))
}
return builder.String()
}
func deviceGetRandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := range result {
result[i] = charset[rng.Intn(len(charset))]
}
return string(result)
}
// NewDevice 创建新的设备信息
func NewDevice() *Device {
imsiMD5 := make([]byte, 16)
rng.Read(imsiMD5)
imsiMD5Arr := md5.Sum(imsiMD5)
fingerprint := fmt.Sprintf(
"xiaomi/iarim/sagit:10/eomam.200122.001/%d:user/release-keys",
rng.Intn(9000000)+1000000,
)
device := &Device{
Display: fmt.Sprintf("QMAPI.%d.001", rng.Intn(900000)+100000),
Product: "iarim",
Device: "sagit",
Board: "eomam",
Model: "MI 6",
Fingerprint: fingerprint,
BootID: uuid.New().String(),
ProcVersion: fmt.Sprintf(
"Linux 5.4.0-54-generic-%s (android-build@google.com)",
deviceGetRandomString(8),
),
IMEI: deviceGetRandomIMEI(),
Brand: "Xiaomi",
Bootloader: "U-boot",
BaseBand: "",
Version: newOSVersion(),
SimInfo: "T-Mobile",
OsType: "android",
MacAddress: "00:50:56:C0:00:08",
WifiBSSID: "00:50:56:C0:00:08",
WifiSSID: "<unknown ssid>",
IMSIMD5: imsiMD5Arr[:],
AndroidID: deviceGetRandomHex(8),
APN: "wifi",
VendorName: "MIUI",
VendorOSName: "qmapi",
Qimei: "",
}
return device
}

View File

@@ -0,0 +1,309 @@
// From https://github.com/fred913/goqrcdec/blob/main/goqrcdec.go
// Under Apache License
package goqrcdec
import (
"bytes"
"compress/zlib"
"fmt"
"io"
"os"
)
// qrcKey used for 3DES decryption
const qrcKey = "!@#)(*$%123ZXC!@!@#)(NHL"
// --- Bitmanipulation helpers ---
func bitNumKeyByte(a []byte, b, c int) uint32 {
i := (b/32)*4 + 3 - (b%32)/8
return uint32(((uint32(a[i]) >> (7 - (b % 8))) & 0x01) << uint(c))
}
// bitNumKey extracts bit b from a slice of uint32 (key bytes) and shifts it c places.
func bitNumKey(a []uint32, b, c int) uint32 {
i := (b/32)*4 + 3 - (b%32)/8
return ((a[i] >> (7 - uint(b%8))) & 0x01) << uint(c)
}
// bitNumIntr extracts a bit from a uint32 and shifts it.
func bitNumIntr(a uint32, b, c int) uint32 {
return ((a >> (31 - uint(b))) & 0x01) << uint(c)
}
// bitNumIntl shifts and masks a uint32 for DES expansion/permutation.
func bitNumIntl(a uint32, b, c int) uint32 {
return ((a << uint(b)) & 0x80000000) >> uint(c)
}
// sboxBit remaps a 6-bit block for S-Box indexing.
func sboxBit(a byte) byte {
return (((a) & 0x20) | (((a) & 0x1f) >> 1) | (((a) & 0x01) << 4))
}
// --- S-Boxes ---
var sbox1 = [64]uint32{
14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2,
13, 1, 10, 6, 12, 11, 9, 5, 3, 8, 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7,
3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13,
}
var sbox2 = [64]uint32{
15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 3, 13, 4, 7, 15, 2,
8, 15, 12, 0, 1, 10, 6, 9, 11, 5, 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6,
9, 3, 2, 15, 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9,
}
var sbox3 = [64]uint32{
10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 13, 7, 0, 9, 3, 4, 6,
10, 2, 8, 5, 14, 12, 11, 15, 1, 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5,
10, 14, 7, 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12,
}
var sbox4 = [64]uint32{
7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 13, 8, 11, 5, 6, 15,
0, 3, 4, 7, 2, 12, 1, 10, 14, 9, 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14,
5, 2, 8, 4, 3, 15, 0, 6, 10, 10, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14,
}
var sbox5 = [64]uint32{
2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 14, 11, 2, 12, 4, 7,
13, 1, 5, 0, 15, 10, 3, 9, 8, 6, 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5,
6, 3, 0, 14, 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3,
}
var sbox6 = [64]uint32{
12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 10, 15, 4, 2, 7, 12,
9, 5, 6, 1, 13, 14, 0, 11, 3, 8, 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1,
13, 11, 6, 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13,
}
var sbox7 = [64]uint32{
4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 13, 0, 11, 7, 4, 9,
1, 10, 14, 3, 5, 12, 2, 15, 8, 6, 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8,
0, 5, 9, 2, 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12,
}
var sbox8 = [64]uint32{
13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3,
7, 4, 12, 5, 6, 11, 0, 14, 9, 2, 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13,
15, 3, 5, 8, 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11,
}
// --- Key schedule for single DES ---
func desKeySetup(key []uint32, schedule *[16][6]byte, mode string) {
keyRndShift := [16]int{1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1}
keyPermC := [...]int{56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35}
keyPermD := [...]int{62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3}
keyCompression := [...]int{13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47, 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31}
var C, D uint32
for i, v := range keyPermC {
C |= bitNumKey(key, v, 31-i)
}
for i, v := range keyPermD {
D |= bitNumKey(key, v, 31-i)
}
for i := 0; i < 16; i++ {
shift := keyRndShift[i]
C = ((C << shift) | (C >>
(28 - shift))) & 0xfffffff0
D = ((D << shift) | (D >>
(28 - shift))) & 0xfffffff0
subkeyIdx := i
if mode == "decrypt" {
subkeyIdx = 15 - i
}
for j := range schedule[subkeyIdx] {
schedule[subkeyIdx][j] = 0
}
for j := 0; j < 24; j++ {
schedule[subkeyIdx][j/8] |= byte(bitNumIntr(C, keyCompression[j], 7-(j%8)))
}
for j := 24; j < 48; j++ {
schedule[subkeyIdx][j/8] |= byte(bitNumIntr(D, keyCompression[j]-27, 7-(j%8)))
}
}
}
// --- Initial & inverse permutations ---
func ip(state *[2]uint32, input []byte) error {
if len(input) < 8 {
return fmt.Errorf("input block too small")
}
state[0] = bitNumKeyByte(input, 57, 31) | bitNumKeyByte(input, 49, 30) | bitNumKeyByte(input, 41, 29) | bitNumKeyByte(input, 33, 28) |
bitNumKeyByte(input, 25, 27) | bitNumKeyByte(input, 17, 26) | bitNumKeyByte(input, 9, 25) | bitNumKeyByte(input, 1, 24) |
bitNumKeyByte(input, 59, 23) | bitNumKeyByte(input, 51, 22) | bitNumKeyByte(input, 43, 21) | bitNumKeyByte(input, 35, 20) |
bitNumKeyByte(input, 27, 19) | bitNumKeyByte(input, 19, 18) | bitNumKeyByte(input, 11, 17) | bitNumKeyByte(input, 3, 16) |
bitNumKeyByte(input, 61, 15) | bitNumKeyByte(input, 53, 14) | bitNumKeyByte(input, 45, 13) | bitNumKeyByte(input, 37, 12) |
bitNumKeyByte(input, 29, 11) | bitNumKeyByte(input, 21, 10) | bitNumKeyByte(input, 13, 9) | bitNumKeyByte(input, 5, 8) |
bitNumKeyByte(input, 63, 7) | bitNumKeyByte(input, 55, 6) | bitNumKeyByte(input, 47, 5) | bitNumKeyByte(input, 39, 4) |
bitNumKeyByte(input, 31, 3) | bitNumKeyByte(input, 23, 2) | bitNumKeyByte(input, 15, 1) | bitNumKeyByte(input, 7, 0)
state[1] = bitNumKeyByte(input, 56, 31) | bitNumKeyByte(input, 48, 30) | bitNumKeyByte(input, 40, 29) | bitNumKeyByte(input, 32, 28) |
bitNumKeyByte(input, 24, 27) | bitNumKeyByte(input, 16, 26) | bitNumKeyByte(input, 8, 25) | bitNumKeyByte(input, 0, 24) |
bitNumKeyByte(input, 58, 23) | bitNumKeyByte(input, 50, 22) | bitNumKeyByte(input, 42, 21) | bitNumKeyByte(input, 34, 20) |
bitNumKeyByte(input, 26, 19) | bitNumKeyByte(input, 18, 18) | bitNumKeyByte(input, 10, 17) | bitNumKeyByte(input, 2, 16) |
bitNumKeyByte(input, 60, 15) | bitNumKeyByte(input, 52, 14) | bitNumKeyByte(input, 44, 13) | bitNumKeyByte(input, 36, 12) |
bitNumKeyByte(input, 28, 11) | bitNumKeyByte(input, 20, 10) | bitNumKeyByte(input, 12, 9) | bitNumKeyByte(input, 4, 8) |
bitNumKeyByte(input, 62, 7) | bitNumKeyByte(input, 54, 6) | bitNumKeyByte(input, 46, 5) | bitNumKeyByte(input, 38, 4) |
bitNumKeyByte(input, 30, 3) | bitNumKeyByte(input, 22, 2) | bitNumKeyByte(input, 14, 1) | bitNumKeyByte(input, 6, 0)
return nil
}
func invIP(state *[2]uint32, data []byte) error {
if len(data) < 8 {
return fmt.Errorf("input block too small")
}
data[3] = byte(bitNumIntr(state[1], 7, 7) | bitNumIntr(state[0], 7, 6) | bitNumIntr(state[1], 15, 5) | bitNumIntr(state[0], 15, 4) | bitNumIntr(state[1], 23, 3) | bitNumIntr(state[0], 23, 2) | bitNumIntr(state[1], 31, 1) | bitNumIntr(state[0], 31, 0))
data[2] = byte(bitNumIntr(state[1], 6, 7) | bitNumIntr(state[0], 6, 6) | bitNumIntr(state[1], 14, 5) | bitNumIntr(state[0], 14, 4) | bitNumIntr(state[1], 22, 3) | bitNumIntr(state[0], 22, 2) | bitNumIntr(state[1], 30, 1) | bitNumIntr(state[0], 30, 0))
data[1] = byte(bitNumIntr(state[1], 5, 7) | bitNumIntr(state[0], 5, 6) | bitNumIntr(state[1], 13, 5) | bitNumIntr(state[0], 13, 4) | bitNumIntr(state[1], 21, 3) | bitNumIntr(state[0], 21, 2) | bitNumIntr(state[1], 29, 1) | bitNumIntr(state[0], 29, 0))
data[0] = byte(bitNumIntr(state[1], 4, 7) | bitNumIntr(state[0], 4, 6) | bitNumIntr(state[1], 12, 5) | bitNumIntr(state[0], 12, 4) | bitNumIntr(state[1], 20, 3) | bitNumIntr(state[0], 20, 2) | bitNumIntr(state[1], 28, 1) | bitNumIntr(state[0], 28, 0))
data[7] = byte(bitNumIntr(state[1], 3, 7) | bitNumIntr(state[0], 3, 6) | bitNumIntr(state[1], 11, 5) | bitNumIntr(state[0], 11, 4) | bitNumIntr(state[1], 19, 3) | bitNumIntr(state[0], 19, 2) | bitNumIntr(state[1], 27, 1) | bitNumIntr(state[0], 27, 0))
data[6] = byte(bitNumIntr(state[1], 2, 7) | bitNumIntr(state[0], 2, 6) | bitNumIntr(state[1], 10, 5) | bitNumIntr(state[0], 10, 4) | bitNumIntr(state[1], 18, 3) | bitNumIntr(state[0], 18, 2) | bitNumIntr(state[1], 26, 1) | bitNumIntr(state[0], 26, 0))
data[5] = byte(bitNumIntr(state[1], 1, 7) | bitNumIntr(state[0], 1, 6) | bitNumIntr(state[1], 9, 5) | bitNumIntr(state[0], 9, 4) | bitNumIntr(state[1], 17, 3) | bitNumIntr(state[0], 17, 2) | bitNumIntr(state[1], 25, 1) | bitNumIntr(state[0], 25, 0))
data[4] = byte(bitNumIntr(state[1], 0, 7) | bitNumIntr(state[0], 0, 6) | bitNumIntr(state[1], 8, 5) | bitNumIntr(state[0], 8, 4) | bitNumIntr(state[1], 16, 3) | bitNumIntr(state[0], 16, 2) | bitNumIntr(state[1], 24, 1) | bitNumIntr(state[0], 24, 0))
return nil
}
// --- DES round function ---
func fFunc(state uint32, key [6]byte) uint32 {
var lrgState [6]byte
t1 := (bitNumIntl(state, 31, 0) | ((state & 0xf0000000) >> 1) | bitNumIntl(state, 4, 5) | bitNumIntl(state, 3, 6) | ((state & 0x0f000000) >> 3) | bitNumIntl(state, 8, 11) | bitNumIntl(state, 7, 12) | ((state & 0x00f00000) >> 5) | bitNumIntl(state, 12, 17) | bitNumIntl(state, 11, 18) | ((state & 0x000f0000) >> 7) | bitNumIntl(state, 16, 23))
t2 := (bitNumIntl(state, 15, 0) | ((state & 0x0000f000) << 15) | bitNumIntl(state, 20, 5) | bitNumIntl(state, 19, 6) | ((state & 0x00000f00) << 13) | bitNumIntl(state, 24, 11) | bitNumIntl(state, 23, 12) | ((state & 0x000000f0) << 11) | bitNumIntl(state, 28, 17) | bitNumIntl(state, 27, 18) | ((state & 0x0000000f) << 9) | bitNumIntl(state, 0, 23))
lrgState[0] = byte((t1 >> 24) & 0xff)
lrgState[1] = byte((t1 >> 16) & 0xff)
lrgState[2] = byte((t1 >> 8) & 0xff)
lrgState[3] = byte((t2 >> 24) & 0xff)
lrgState[4] = byte((t2 >> 16) & 0xff)
lrgState[5] = byte((t2 >> 8) & 0xff)
for i := range 6 {
lrgState[i] ^= key[i]
}
// S-Box
out := ((sbox1[sboxBit((lrgState[0]>>2))] << 28) | (sbox2[sboxBit((((lrgState[0]&0x03)<<4)|(lrgState[1]>>4)))] << 24) | (sbox3[sboxBit((((lrgState[1]&0x0f)<<2)|(lrgState[2]>>6)))] << 20) | (sbox4[sboxBit((lrgState[2]&0x3f))] << 16) | (sbox5[sboxBit((lrgState[3]>>2))] << 12) | (sbox6[sboxBit((((lrgState[3]&0x03)<<4)|(lrgState[4]>>4)))] << 8) | (sbox7[sboxBit((((lrgState[4]&0x0f)<<2)|(lrgState[5]>>6)))] << 4) | sbox8[sboxBit((lrgState[5]&0x3f))])
// P-Box
out = (bitNumIntl(out, 15, 0) | bitNumIntl(out, 6, 1) | bitNumIntl(out, 19, 2) | bitNumIntl(out, 20, 3) | bitNumIntl(out, 28, 4) | bitNumIntl(out, 11, 5) | bitNumIntl(out, 27, 6) | bitNumIntl(out, 16, 7) | bitNumIntl(out, 0, 8) | bitNumIntl(out, 14, 9) | bitNumIntl(out, 22, 10) | bitNumIntl(out, 25, 11) | bitNumIntl(out, 4, 12) | bitNumIntl(out, 17, 13) | bitNumIntl(out, 30, 14) | bitNumIntl(out, 9, 15) | bitNumIntl(out, 1, 16) | bitNumIntl(out, 7, 17) | bitNumIntl(out, 23, 18) | bitNumIntl(out, 13, 19) | bitNumIntl(out, 31, 20) | bitNumIntl(out, 26, 21) | bitNumIntl(out, 2, 22) | bitNumIntl(out, 8, 23) | bitNumIntl(out, 18, 24) | bitNumIntl(out, 12, 25) | bitNumIntl(out, 29, 26) | bitNumIntl(out, 5, 27) | bitNumIntl(out, 21, 28) | bitNumIntl(out, 10, 29) | bitNumIntl(out, 3, 30) | bitNumIntl(out, 24, 31))
return out
}
// --- 3DES wrapper ---
func desCrypt(dataIn []byte, dataOut []byte, key [16][6]byte) error {
var state [2]uint32
var err error = nil
err = ip(&state, dataIn)
if err != nil {
return err
}
for idx := range 15 {
t := state[1]
state[1] = fFunc(state[1], key[idx]) ^ state[0]
state[0] = t
}
state[0] = fFunc(state[1], key[15]) ^ state[0]
err = invIP(&state, dataOut)
return err
}
func threeDesKeySetup(key []uint32, schedule *[3][16][6]byte, mode string) error {
if mode == "encrypt" {
desKeySetup(key, &schedule[0], "encrypt")
desKeySetup(key[8:], &schedule[1], "decrypt")
desKeySetup(key[16:], &schedule[2], "encrypt")
} else if mode == "decrypt" {
desKeySetup(key, &schedule[2], "decrypt")
desKeySetup(key[8:], &schedule[1], "encrypt")
desKeySetup(key[16:], &schedule[0], "decrypt")
} else {
return fmt.Errorf("invalid mode %q", mode)
}
return nil
}
func threeDesCrypt(dataIn []byte, dataOut []byte, key *[3][16][6]byte) error {
var err error = nil
err = desCrypt(dataIn, dataOut, key[0])
if err != nil {
return err
}
err = desCrypt(dataOut, dataOut, key[1])
if err != nil {
return err
}
err = desCrypt(dataOut, dataOut, key[2])
return err
}
// DecodeQRC decrypts and decompresses QRC data.
func DecodeQRC(data []byte) ([]byte, error) {
if len(data) > 11 && string(data[:11]) == "[offset:0]\n" {
data = data[11:]
}
srcLen := len(data)
var schedule [3][16][6]byte
// build key slice
keyBytes := []byte(qrcKey)
key := make([]uint32, len(keyBytes))
for i, b := range keyBytes {
key[i] = uint32(b)
}
var err error = nil
err = threeDesKeySetup(key, &schedule, "decrypt")
if err != nil {
return nil, fmt.Errorf("failed to init key: %w", err)
}
// decrypt each 8-byte block
newData := make([]byte, srcLen)
for i := 0; i < srcLen; i += 8 {
blockLen := 8
if i+8 > srcLen {
blockLen = srcLen - i
}
var inBlock [8]byte
var outBlock [8]byte
copy(inBlock[:], data[i:i+blockLen])
copy(outBlock[:], data[i:i+blockLen])
err = threeDesCrypt(data[i:], outBlock[:], &schedule)
if err != nil {
return nil, fmt.Errorf("failed to decrypt block %d: %w", i/8, err)
}
copy(newData[i:], outBlock[:blockLen])
}
zr, err := zlib.NewReader(bytes.NewReader(newData))
if err != nil {
return nil, fmt.Errorf("invalid zlib data: %w", err)
}
defer zr.Close()
result, err := io.ReadAll(zr)
if err != nil {
return nil, fmt.Errorf("decompression failed: %w", err)
}
// strip UTF-8 BOM if present
if len(result) >= 3 && result[0] == 0xEF && result[1] == 0xBB && result[2] == 0xBF {
result = result[3:]
}
return result, nil
}
// DecodeFile reads and decodes a QRC file from disk.
func DecodeFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
return DecodeQRC(data)
}

12
providers/qq/init.go Normal file
View File

@@ -0,0 +1,12 @@
package qq
import (
"github.com/AynaLivePlayer/miaosic"
"math/rand"
"time"
)
func init() {
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
miaosic.RegisterProvider(NewQQMusicProvider())
}

108
providers/qq/login.go Normal file
View File

@@ -0,0 +1,108 @@
package qq
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/AynaLivePlayer/miaosic"
)
func (p *QQMusicProvider) Login(username string, password string) error {
return miaosic.ErrNotImplemented
}
func (p *QQMusicProvider) Logout() error {
// todo api request
p.cred = NewCredential()
return miaosic.ErrNotImplemented
}
func (p *QQMusicProvider) IsLogin() bool {
// todo check if token expires
return p.cred.HasMusicID() && p.cred.HasMusicKey()
}
func (p *QQMusicProvider) refreshToken() error {
if p.cred.RefreshKey == "" || p.cred.RefreshToken == "" || !p.cred.HasMusicKey() || !p.cred.HasMusicKey() {
return errors.New("miaosic (qq): invalid credentials")
}
params := map[string]interface{}{
"refresh_key": p.cred.RefreshKey,
"refresh_token": p.cred.RefreshToken,
"musickey": p.cred.MusicKey,
"musicid": p.cred.MusicID,
}
data, err := p.makeApiRequest("music.login.LoginServer",
"Login", params)
if err != nil {
return err
}
if !data.Get("data.musickey").Exists() || data.Get("data.musickey").String() == "" {
return errors.New("miaosic (qq): fail to get login status data")
}
p.cred.OpenID = data.Get("data.openid").String()
p.cred.RefreshToken = data.Get("data.refresh_token").String()
p.cred.AccessToken = data.Get("data.access_token").String()
p.cred.ExpiredAt = data.Get("data.expired_at").Int()
p.cred.MusicID = data.Get("data.musicid").Int()
p.cred.MusicKey = data.Get("data.musickey").String()
p.cred.UnionID = data.Get("data.unionid").String()
p.cred.StrMusicID = data.Get("data.str_musicid").String()
p.cred.RefreshKey = data.Get("data.refresh_key").String()
p.cred.EncryptUin = data.Get("data.encryptUin").String()
p.cred.LoginType = int(data.Get("data.loginType").Int())
return nil
}
func (p *QQMusicProvider) QrLogin() (*miaosic.QrLoginSession, error) {
// todo finish wechat qrlogin channel
return p.getQQQR()
}
func (p *QQMusicProvider) QrLoginVerify(qrlogin *miaosic.QrLoginSession) (*miaosic.QrLoginResult, error) {
// todo finish wechat qrlogin channel
return p.checkQQQR(qrlogin)
}
func (p *QQMusicProvider) RestoreSession(session string) error {
if session == "" {
return errors.New("miaosic (qq): session is empty")
}
b, err := base64.StdEncoding.DecodeString(session)
if err != nil {
return err
}
var data struct {
Device *Device `json:"device"`
Credential *Credential `json:"credential"`
}
err = json.Unmarshal(b, &data)
if err != nil {
return fmt.Errorf("miaosic (qq): failed to unmarshal session data: %w", err)
}
if data.Device == nil {
return errors.New("miaosic (qq): missing device info in session")
}
if data.Credential == nil {
return errors.New("miaosic (qq): missing credential info in session")
}
p.device = data.Device
p.cred = data.Credential
p.qimeiUpdated = false
return nil
}
func (p *QQMusicProvider) SaveSession() string {
data := map[string]interface{}{
"device": p.device,
"credential": p.cred,
}
val, _ := json.Marshal(data)
return base64.StdEncoding.EncodeToString(val)
}

274
providers/qq/login_qq.go Normal file
View File

@@ -0,0 +1,274 @@
package qq
import (
"bytes"
"errors"
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
"github.com/spf13/cast"
"image"
_ "image/png" // qq qrcode is png
"net/url"
"regexp"
"strconv"
"time"
"github.com/makiuchi-d/gozxing"
"github.com/makiuchi-d/gozxing/qrcode"
)
func (p *QQMusicProvider) getQQQR() (*miaosic.QrLoginSession, error) {
resp, err := miaosic.Requester.GetQuery(
"https://ssl.ptlogin2.qq.com/ptqrshow",
map[string]string{
"appid": "716027609",
"e": "2",
"l": "M",
"s": "3",
"d": "72",
"v": "4",
"t": cast.ToString(rng.Float64()),
"daid": "383",
"pt_3rd_aid": "100497308",
},
map[string]string{
"Referer": "https://ssl.ptlogin2.qq.com/",
},
)
if err != nil {
return nil, err
}
// 获取qrsig cookie
var qrsig string
for _, cookie := range resp.RawResponse.Cookies() {
if cookie.Name == "qrsig" {
qrsig = cookie.Value
break
}
}
if qrsig == "" {
return nil, errors.New("miaosic (qq): failed to get qrsig")
}
img, _, err := image.Decode(bytes.NewBuffer(resp.Body()))
if err != nil {
return nil, errors.New("miaosic (qq): failed to read qrcode")
}
bmp, err := gozxing.NewBinaryBitmapFromImage(img)
if err != nil {
return nil, errors.New("miaosic (qq): failed to read qrcode to bmp")
}
qrReader := qrcode.NewQRCodeReader()
result, err := qrReader.Decode(bmp, nil)
if err != nil {
return nil, errors.New("miaosic (qq): failed to decode qrcode")
}
return &miaosic.QrLoginSession{
Url: result.GetText(),
Key: qrsig,
}, nil
}
func (p *QQMusicProvider) checkQQQR(qrlogin *miaosic.QrLoginSession) (*miaosic.QrLoginResult, error) {
ptqrtoken := strconv.Itoa(hash33(qrlogin.Key, 0))
resp, err := miaosic.Requester.GetQuery(
"https://ssl.ptlogin2.qq.com/ptqrlogin",
map[string]string{
"u1": "https://graph.qq.com/oauth2.0/login_jump",
"ptqrtoken": ptqrtoken,
"ptredirect": "0",
"h": "1",
"t": "1",
"g": "1",
"from_ui": "1",
"ptlang": "2052",
"action": fmt.Sprintf("0-0-%d", time.Now().UnixMilli()),
"js_ver": "20102616",
"js_type": "1",
"pt_uistyle": "40",
"aid": "716027609",
"daid": "383",
"pt_3rd_aid": "100497308",
"has_onekey": "1",
},
map[string]string{
"Referer": "https://xui.ptlogin2.qq.com/",
"Cookie": "qrsig=" + qrlogin.Key,
},
)
if err != nil {
return &miaosic.QrLoginResult{Success: false, Message: "http error, might be invalid qrsig"}, err
}
// 使用正则表达式提取状态码
re := regexp.MustCompile(`ptuiCB\('(\d+)','0','(.*?)','0','(.*?)', (.*?)'\)`)
matches := re.FindStringSubmatch(resp.String())
if len(matches) < 5 {
return &miaosic.QrLoginResult{Success: false, Message: "invalid response"}, errors.New("miaosic (qq): invalid response format")
}
statusCode, _ := strconv.Atoi(matches[1])
//DONE = (0, 405)
//SCAN = (66, 408)
//CONF = (67, 404)
//TIMEOUT = (65, None)
//REFUSE = (68, 403)
//OTHER = (None, None)
switch statusCode {
case 0:
return p.authorizeQQQR(qrlogin, matches[2])
case 66:
return &miaosic.QrLoginResult{Success: false, Message: "等待扫描二维码"}, nil
case 67:
return &miaosic.QrLoginResult{Success: false, Message: "扫描未确认登陆"}, nil
case 65:
return &miaosic.QrLoginResult{Success: false, Message: "二维码已过期"}, nil
case 68:
return &miaosic.QrLoginResult{Success: false, Message: "! 拒绝登陆 !"}, nil
default:
return &miaosic.QrLoginResult{Success: false, Message: matches[3]}, nil
}
}
func (p *QQMusicProvider) authorizeQQQR(qrlogin *miaosic.QrLoginSession, urlStr string) (*miaosic.QrLoginResult, error) {
// 解析URL
u, err := url.Parse(urlStr)
if err != nil {
return &miaosic.QrLoginResult{Success: false, Message: "invalid response"}, nil
}
// 提取参数
params := u.Query()
uin := params.Get("uin")
sigx := params.Get("ptsigx")
if uin == "" || sigx == "" {
return &miaosic.QrLoginResult{Success: false, Message: "invalid response"}, errors.New("miaosic (qq): missing uin or sigx")
}
//// 使用 Requester 发送 GET 请求
//resp, err := miaosic.Requester.GetQuery(
// urlStr,
// map[string]string{},
// map[string]string{
// "Referer": "https://xui.ptlogin2.qq.com/",
// "Cookie": "qrsig=" + qrlogin.Key,
// },
//)
//if err != nil {
// return nil, err
//}
respR, err := resty.New().
SetRedirectPolicy(resty.NoRedirectPolicy()).
R().
SetHeader("Referer", "https://xui.ptlogin2.qq.com/").
Get(urlStr)
if respR == nil {
return &miaosic.QrLoginResult{Success: false, Message: "invalid response"}, nil
}
// 获取p_skey cookie
var pSkey string
for _, cookie := range respR.RawResponse.Cookies() {
if cookie.Name == "p_skey" {
pSkey = cookie.Value
break
}
}
if pSkey == "" {
return &miaosic.QrLoginResult{Success: false, Message: "failed to get p_skey"}, errors.New("miaosic (qq): failed to get p_skey")
}
gTk := hash33(pSkey, 5381)
// 构建表单数据
formData := url.Values{}
formData.Set("response_type", "code")
formData.Set("client_id", "100497308")
formData.Set("redirect_uri", "https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https://y.qq.com/")
formData.Set("scope", "get_user_info,get_app_friends")
formData.Set("state", "state")
formData.Set("switch", "")
formData.Set("from_ptlogin", "1")
formData.Set("src", "1")
formData.Set("update_auth", "1")
formData.Set("openapi", "1010_1030")
formData.Set("g_tk", strconv.Itoa(gTk))
formData.Set("auth_time", strconv.FormatInt(time.Now().UnixMilli(), 10))
formData.Set("ui", uuid.New().String())
respR, err = resty.New().
SetRedirectPolicy(resty.NoRedirectPolicy()).
R().
SetCookies(respR.Cookies()).
SetFormDataFromValues(formData).
SetHeaders(map[string]string{
"Referer": "https://xui.ptlogin2.qq.com/",
}).Post("https://graph.qq.com/oauth2.0/authorize")
//resp, err := miaosic.Requester.Post(
// "https://graph.qq.com/oauth2.0/authorize",
// map[string]string{
// "Content-Type": "application/x-www-form-urlencoded",
// "Referer": "https://xui.ptlogin2.qq.com/",
// },
// formData.Encode(),
//)
if respR == nil {
return &miaosic.QrLoginResult{Success: false, Message: "oauth failed, access failed"}, nil
}
location := respR.Header().Get("Location")
if location == "" {
return &miaosic.QrLoginResult{Success: false, Message: "oauth failed, no location found"}, nil
}
u, err = url.Parse(location)
if err != nil {
return nil, err
}
code := u.Query().Get("code")
if code == "" {
return &miaosic.QrLoginResult{Success: false, Message: "oauth failed, no code in redirection location"}, nil
}
return p.getCredentialWithCode(code, 2) // 2 表示QQ登录
}
func (p *QQMusicProvider) getCredentialWithCode(code string, loginType int) (*miaosic.QrLoginResult, error) {
p.cred.LoginType = loginType
params := map[string]interface{}{
"code": code,
}
data, err := p.makeApiRequest("QQConnectLogin.LoginServer", "QQLogin", params)
if err != nil {
return nil, err
}
if !data.Get("data.musickey").Exists() || data.Get("data.musickey").String() == "" {
return &miaosic.QrLoginResult{Success: false, Message: "fail to get status data"}, nil
}
p.cred.OpenID = data.Get("data.openid").String()
p.cred.RefreshToken = data.Get("data.refresh_token").String()
p.cred.AccessToken = data.Get("data.access_token").String()
p.cred.ExpiredAt = data.Get("data.expired_at").Int()
p.cred.MusicID = data.Get("data.musicid").Int()
p.cred.MusicKey = data.Get("data.musickey").String()
p.cred.UnionID = data.Get("data.unionid").String()
p.cred.StrMusicID = data.Get("data.str_musicid").String()
p.cred.RefreshKey = data.Get("data.refresh_key").String()
p.cred.EncryptUin = data.Get("data.encryptUin").String()
p.cred.LoginType = int(data.Get("data.loginType").Int())
return &miaosic.QrLoginResult{Success: true, Message: "ok"}, nil
}

View File

@@ -0,0 +1,12 @@
package qq
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestQQ_getQrcodeQQ(t *testing.T) {
_, err := testApi.getQQQR()
require.NoError(t, err)
//pp.Println(result)
}

89
providers/qq/playlist.go Normal file
View File

@@ -0,0 +1,89 @@
package qq
import (
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/spf13/cast"
"github.com/tidwall/gjson"
"regexp"
"strings"
)
var playlistRegexp = regexp.MustCompile(`ryqq/playlist/(\d+)`)
var playlistRegexp0 = regexp.MustCompile("^[0-9]+$")
func (p *QQMusicProvider) MatchPlaylist(uri string) (miaosic.MetaData, bool) {
if id := playlistRegexp0.FindString(uri); id != "" {
return miaosic.MetaData{
Provider: p.GetName(),
Identifier: id,
}, true
}
if id := playlistRegexp.FindString(uri); id != "" {
return miaosic.MetaData{
Provider: p.GetName(),
Identifier: id[14:],
}, true
}
return miaosic.MetaData{}, false
}
func (p *QQMusicProvider) getPlaylist(meta miaosic.MetaData, page, pagesize int) (gjson.Result, error) {
params := map[string]interface{}{
"disstid": cast.ToInt(meta.Identifier),
"dirid": 0,
"tag": 0,
"song_begin": pagesize * (page - 1),
"song_num": pagesize,
"userinfo": 0,
"orderlist": 1,
"onlysonglist": 0,
}
return p.makeApiRequest("music.srfDissInfo.DissInfo", "CgiGetDiss", params)
}
func (p *QQMusicProvider) GetPlaylist(meta miaosic.MetaData) (*miaosic.Playlist, error) {
// todo use RequestGroup.
playlist := &miaosic.Playlist{
Meta: meta,
Title: "QQPlaylist " + meta.Identifier,
Medias: make([]miaosic.MediaInfo, 0),
}
for page := 1; page < 20; page++ {
data, err := p.getPlaylist(meta, page, 100)
if err != nil {
return nil, err
}
totalSongNum := int(data.Get("data.total_song_num").Int())
data.Get("data.songlist").ForEach(func(k, info gjson.Result) bool {
albumMid := info.Get("album.mid").String()
coverURL := ""
if albumMid != "" {
coverURL = fmt.Sprintf("https://y.qq.com/music/photo_new/T002R500x500M000%s.jpg", albumMid)
}
var artistNames []string
info.Get("singer").ForEach(func(_, singer gjson.Result) bool {
name := singer.Get("name").String()
if name != "" {
artistNames = append(artistNames, name)
}
return true
})
playlist.Medias = append(playlist.Medias, miaosic.MediaInfo{
Title: info.Get("title").String(),
Artist: strings.Join(artistNames, ","),
Album: info.Get("album.title").String(),
Cover: miaosic.Picture{Url: coverURL},
Meta: miaosic.MetaData{Provider: p.GetName(), Identifier: info.Get("mid").String()},
})
return true
})
if page == 1 {
playlist.Title = data.Get("data.dirinfo.title").String()
}
if len(playlist.Medias) >= totalSongNum {
break
}
}
return playlist, nil
}

View File

@@ -0,0 +1,27 @@
package qq
import (
"github.com/k0kubun/pp/v3"
"github.com/stretchr/testify/require"
"testing"
)
func TestQQMusicProvider_MatchPlaylist(t *testing.T) {
val1, ok1 := testApi.MatchPlaylist("https://y.qq.com/n/ryqq/playlist/9515850830")
require.True(t, ok1)
require.Equal(t, "9515850830", val1.Identifier)
require.Equal(t, testApi.GetName(), val1.Provider)
val2, ok2 := testApi.MatchPlaylist("9515850830")
require.True(t, ok2)
require.Equal(t, "9515850830", val2.Identifier)
require.Equal(t, testApi.GetName(), val2.Provider)
}
func TestQQMusicProvider_GetPlaylist(t *testing.T) {
val1, ok1 := testApi.MatchPlaylist("https://y.qq.com/n/ryqq/playlist/7426999757")
require.True(t, ok1)
playlist, err := testApi.GetPlaylist(val1)
require.NoError(t, err)
require.True(t, len(playlist.Medias) >= 33)
pp.Println(playlist)
}

278
providers/qq/qimei.go Normal file
View File

@@ -0,0 +1,278 @@
package qq
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/AynaLivePlayer/miaosic"
"strings"
"time"
)
const (
QiMeiPublicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDEIxgwoutfwoJxcGQeedgP7FG9qaIuS0qzfR8gWkrkTZKM2iWHn2ajQpBRZjMSoSf6+KJGvar2ORhBfpDXyVtZCKpqLQ+FLkpncClKVIrBwv6PHyUvuCb0rIarmgDnzkfQAqVufEtR64iazGDKatvJ9y6B9NMbHddGSAUmRTCrHQIDAQAB
-----END PUBLIC KEY-----`
QiMeiSecret = "ZdJqM15EeO2zWc08"
QiMeiAppKey = "0AND0HD6FE4HY80F"
)
type QimeiResult struct {
Q16 string `json:"q16"`
Q36 string `json:"q36"`
}
type qimeiReserved struct {
Harmony string `json:"harmony"`
Clone string `json:"clone"`
Containe string `json:"containe"`
Oz string `json:"oz"`
Oo string `json:"oo"`
Kelong string `json:"kelong"`
Uptimes string `json:"uptimes"`
MultiUser string `json:"multiUser"`
Bod string `json:"bod"`
Dv string `json:"dv"`
FirstLevel string `json:"firstLevel"`
Manufact string `json:"manufact"`
Name string `json:"name"`
Host string `json:"host"`
Kernel string `json:"kernel"`
}
type qimeiPayload struct {
AndroidID string `json:"androidId"`
PlatformID int `json:"platformId"`
AppKey string `json:"appKey"`
AppVersion string `json:"appVersion"`
BeaconIDSrc string `json:"beaconIdSrc"`
Brand string `json:"brand"`
ChannelID string `json:"channelId"`
Cid string `json:"cid"`
Imei string `json:"imei"`
Imsi string `json:"imsi"`
Mac string `json:"mac"`
Model string `json:"model"`
NetworkType string `json:"networkType"`
Oaid string `json:"oaid"`
OSVersion string `json:"osVersion"`
Qimei string `json:"qimei"`
Qimei36 string `json:"qimei36"`
SDKVersion string `json:"sdkVersion"`
TargetSDKVersion string `json:"targetSdkVersion"`
Audit string `json:"audit"`
UserID string `json:"userId"`
PackageID string `json:"packageId"`
DeviceType string `json:"deviceType"`
SDKName string `json:"sdkName"`
Reserved string `json:"reserved"`
}
type qimeiParams struct {
Key string `json:"key"`
Params string `json:"params"`
Time string `json:"time"`
Nonce string `json:"nonce"`
Sign string `json:"sign"`
Extra string `json:"extra"`
}
type qimeiRequest struct {
App int `json:"app"`
Os int `json:"os"`
QimeiParams qimeiParams `json:"qimeiParams"`
}
type qimeiResponse struct {
Data string `json:"data"`
}
type qimeiData struct {
Data struct {
Q16 string `json:"q16"`
Q36 string `json:"q36"`
} `json:"data"`
}
func qimeiRandomBeaconID() string {
timeMonth := time.Now().Format("2006-01-") + "01"
rand1 := rng.Intn(900000) + 100000
rand2 := rng.Intn(900000000) + 100000000
var parts []string
for i := 1; i <= 40; i++ {
switch i {
case 1, 2, 13, 14, 17, 18, 21, 22, 25, 26, 29, 30, 33, 34, 37, 38:
parts = append(parts, fmt.Sprintf("k%d:%s%d.%d", i, timeMonth, rand1, rand2))
case 3:
parts = append(parts, "k3:0000000000000000")
case 4:
// 生成16位随机十六进制
buf := make([]byte, 8)
rng.Read(buf)
parts = append(parts, fmt.Sprintf("k4:%x", buf))
default:
parts = append(parts, fmt.Sprintf("k%d:%d", i, rng.Intn(10000)))
}
}
return strings.Join(parts, ";")
}
func qimeiRandomPayloadByDevice(device *Device, version string) *qimeiPayload {
fixedRand := rng.Intn(14400)
uptime := time.Now().Add(-time.Duration(fixedRand) * time.Second).Format("2006-01-02 15:04:05")
reserved := &qimeiReserved{
Harmony: "0",
Clone: "0",
Containe: "",
Oz: "UhYmelwouA+V2nPWbOvLTgN2/m8jwGB+yUB5v9tysQg=",
Oo: "Xecjt+9S1+f8Pz2VLSxgpw==",
Kelong: "0",
Uptimes: uptime,
MultiUser: "0",
Bod: device.Brand,
Dv: device.Device,
FirstLevel: "",
Manufact: device.Brand,
Name: device.Model,
Host: "se.infra",
Kernel: device.ProcVersion,
}
reservedJSON, _ := json.Marshal(reserved)
return &qimeiPayload{
AndroidID: device.AndroidID,
PlatformID: 1,
AppKey: QiMeiAppKey,
AppVersion: version,
BeaconIDSrc: qimeiRandomBeaconID(),
Brand: device.Brand,
ChannelID: "10003505",
Cid: "",
Imei: device.IMEI,
Imsi: "",
Mac: "",
Model: device.Model,
NetworkType: "unknown",
Oaid: "",
OSVersion: fmt.Sprintf("Android %s,level %d", device.Version.Release, device.Version.Sdk),
Qimei: "",
Qimei36: "",
SDKVersion: version,
TargetSDKVersion: "33",
Audit: "",
UserID: "{}",
PackageID: "com.tencent.qqmusic",
DeviceType: "Phone",
SDKName: "",
Reserved: string(reservedJSON),
}
}
func getQimei(device *Device, version string) (*QimeiResult, error) {
result, err := fetchQimei(device, version)
if err == nil {
device.Qimei = result.Q36
return result, nil
}
if device.Qimei != "" {
return &QimeiResult{Q16: "", Q36: device.Qimei}, nil
}
return &QimeiResult{Q16: "", Q36: "6c9d3cd110abca9b16311cee10001e717614"}, nil
}
func qimeiRandomString(length int) string {
const charset = "abcdef0123456789"
result := make([]byte, length)
for i := range result {
result[i] = charset[rng.Intn(len(charset))]
}
return string(result)
}
// 从腾讯API获取QIMEI
func fetchQimei(device *Device, version string) (*QimeiResult, error) {
payload := qimeiRandomPayloadByDevice(device, version)
payloadJSON, err := json.Marshal(payload)
if err != nil {
return nil, err
}
// 生成加密密钥和随机数
cryptKey := qimeiRandomString(16)
nonce := qimeiRandomString(16)
ts := time.Now().UnixMilli()
// RSA加密密钥
rsaEncryptedKey, err := rsaEncrypt([]byte(cryptKey), QiMeiPublicKey)
if err != nil {
return nil, err
}
keyBase64 := base64.StdEncoding.EncodeToString(rsaEncryptedKey)
// AES加密payload
aesEncrypted, err := aesEncrypt([]byte(cryptKey), payloadJSON)
if err != nil {
return nil, err
}
paramsBase64 := base64.StdEncoding.EncodeToString(aesEncrypted)
// 生成签名
extra := fmt.Sprintf(`{"appKey":"%s"}`, QiMeiAppKey)
sign := calcMd5(keyBase64, paramsBase64, fmt.Sprintf("%d", ts), nonce, QiMeiSecret, extra)
// 构建请求
qimeiParams := qimeiParams{
Key: keyBase64,
Params: paramsBase64,
Time: fmt.Sprintf("%d", ts),
Nonce: nonce,
Sign: sign,
Extra: extra,
}
requestBody := qimeiRequest{
App: 0,
Os: 1,
QimeiParams: qimeiParams,
}
requestJSON, err := json.Marshal(requestBody)
if err != nil {
return nil, err
}
resp, err := miaosic.Requester.Post(
"https://api.tencentmusic.com/tme/trpc/proxy",
map[string]string{
"Host": "api.tencentmusic.com",
"method": "GetQimei",
"service": "trpc.tme_datasvr.qimeiproxy.QimeiProxy",
"appid": "qimei_qq_android",
"sign": calcMd5("qimei_qq_androidpzAuCmaFAaFaHrdakPjLIEqKrGnSOOvH", fmt.Sprintf("%d", ts/1000)),
"user-agent": "QQMusic",
"timestamp": fmt.Sprintf("%d", ts/1000),
"Content-Type": "application/json",
}, requestJSON,
)
if err != nil {
return nil, err
}
var qimeiResp qimeiResponse
if err := json.Unmarshal(resp.Body(), &qimeiResp); err != nil {
return nil, err
}
var qimeiData qimeiData
if err := json.Unmarshal([]byte(qimeiResp.Data), &qimeiData); err != nil {
return nil, err
}
return &QimeiResult{
Q16: qimeiData.Data.Q16,
Q36: qimeiData.Data.Q36,
}, nil
}

View File

@@ -0,0 +1,13 @@
package qq
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestGetQimei(t *testing.T) {
device := NewDevice()
qimei, err := getQimei(device, "13.2.5.8")
require.NoError(t, err)
require.NotEqual(t, "6c9d3cd110abca9b16311cee10001e717614", qimei.Q36)
}

258
providers/qq/qq.go Normal file
View File

@@ -0,0 +1,258 @@
package qq
import (
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/tidwall/gjson"
"regexp"
"slices"
"strings"
)
type ApiConfig struct {
Version string
VersionCode int
EnableSign bool
Endpoint string
EncEndpoint string
}
type QQMusicProvider struct {
cfg ApiConfig
device *Device
cred *Credential
header map[string]string
qimeiUpdated bool //i don't care concurrence
tokenRefreshed bool
}
func (p *QQMusicProvider) GetName() string {
return "qq"
}
func (p *QQMusicProvider) Qualities() []miaosic.Quality {
return []miaosic.Quality{
QualityMaster, QualityAtmos2, QualityAtmos51,
QualityFLAC,
QualityOGG640, QualityOGG320, QualityOGG192, QualityOGG96,
QualityMP3320, QualityMP3128, QualityACC192,
QualityACC96, QualityACC48,
}
}
func NewQQMusicProvider() *QQMusicProvider {
val := &QQMusicProvider{
cfg: ApiConfig{
Version: "13.2.5.8",
VersionCode: 13020508,
EnableSign: true,
Endpoint: "https://u.y.qq.com/cgi-bin/musics.fcg",
EncEndpoint: "https://u.y.qq.com/cgi-bin/musics.fcg",
},
cred: NewCredential(),
device: NewDevice(),
header: map[string]string{
"host": "y.qq.com",
"user-agent": "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54",
},
qimeiUpdated: false,
tokenRefreshed: false,
}
return val
}
var idRegexp = regexp.MustCompile(`^\d{3,3}[0-9A-Za-z]{11,11}$`)
func (p *QQMusicProvider) MatchMedia(uri string) (miaosic.MetaData, bool) {
if id := idRegexp.FindString(uri); id != "" {
return miaosic.MetaData{
Provider: p.GetName(),
Identifier: id,
}, true
}
return miaosic.MetaData{}, false
}
func (p *QQMusicProvider) Search(keyword string, page, size int) ([]miaosic.MediaInfo, error) {
params := map[string]interface{}{
"searchid": getSearchID(),
"query": keyword,
"search_type": "item_song",
"num_per_page": size,
"page_num": page,
"highlight": 1, // 1: true
"grp": 1, // 1: true
}
data, err := p.makeApiRequest("music.search.SearchCgiService", "DoSearchForQQMusicMobile", params)
if err != nil {
return nil, err
}
var medias []miaosic.MediaInfo
data.Get("data.body.item_song").ForEach(func(key, info gjson.Result) bool {
title := info.Get("title").String()
mid := info.Get("mid").String()
var artistNames []string
info.Get("singer").ForEach(func(key, value gjson.Result) bool {
name := value.Get("name").String()
if name != "" {
artistNames = append(artistNames, name)
}
return true
})
artist := strings.Join(artistNames, ",")
albumMid := info.Get("album.mid").String()
coverURL := ""
albumSize := 500 //[150, 300, 500, 800]:
if albumMid != "" {
coverURL = fmt.Sprintf("https://y.qq.com/music/photo_new/T002R%dx%dM000%s.jpg", albumSize, albumSize, albumMid)
}
medias = append(medias, miaosic.MediaInfo{
Title: title,
Artist: artist,
Album: info.Get("album.title").String(),
Cover: miaosic.Picture{Url: coverURL},
Meta: miaosic.MetaData{Provider: p.GetName(), Identifier: mid},
})
return true
})
return medias, nil
}
func (p *QQMusicProvider) GetMediaInfo(meta miaosic.MetaData) (miaosic.MediaInfo, error) {
params := map[string]interface{}{
"mids": []string{meta.Identifier},
"types": []int{0},
"modify_stamp": []int{0},
"ctx": 0,
"client": 1,
}
data, err := p.makeApiRequest("music.trackInfo.UniformRuleCtrl", "CgiGetTrackInfo", params)
if err != nil {
return miaosic.MediaInfo{}, err
}
track := data.Get("data.tracks.0")
if !track.Exists() {
return miaosic.MediaInfo{}, fmt.Errorf("miaosic (qq): song not found")
}
title := track.Get("title").String()
mid := track.Get("mid").String()
albumMid := track.Get("album.mid").String()
albumTitle := track.Get("album.title").String()
var artistNames []string
track.Get("singer").ForEach(func(_, singer gjson.Result) bool {
name := singer.Get("name").String()
if name != "" {
artistNames = append(artistNames, name)
}
return true
})
artist := strings.Join(artistNames, ",")
coverURL := ""
if albumMid != "" {
coverURL = fmt.Sprintf("https://y.qq.com/music/photo_new/T002R500x500M000%s.jpg", albumMid)
}
return miaosic.MediaInfo{
Title: title,
Artist: artist,
Album: albumTitle,
Cover: miaosic.Picture{Url: coverURL},
Meta: miaosic.MetaData{Provider: p.GetName(), Identifier: mid},
}, nil
}
func (p *QQMusicProvider) asQQQuality(quality miaosic.Quality) miaosic.Quality {
if slices.Contains(p.Qualities(), quality) {
return quality
}
return QualityMP3320
}
func (p *QQMusicProvider) GetMediaUrl(meta miaosic.MetaData, quality miaosic.Quality) ([]miaosic.MediaUrl, error) {
var module, method string
quality = p.asQQQuality(quality)
if isEncryptedQuality(quality) {
module = "music.vkey.GetEVkey"
method = "CgiGetEVkey"
} else {
module = "music.vkey.GetVkey"
method = "UrlGetVkey"
}
qs := strings.Split(string(quality), ".")
domain := "https://isure.stream.qqmusic.qq.com/"
params := map[string]interface{}{
"filename": []string{fmt.Sprintf("%sOvO%sQwQ.%s", qs[0], meta.Identifier, qs[1])},
"guid": getGuid(),
"songmid": []string{meta.Identifier},
"songtype": []int{0},
}
data, err := p.makeApiRequest(module, method, params)
if err != nil {
return nil, err
}
wifiurl := data.Get("data.midurlinfo.0.wifiurl").String()
if wifiurl == "" {
return nil, fmt.Errorf("miaosic (qq): wifiurl not found, might require vip/no copyright")
}
result := []miaosic.MediaUrl{
miaosic.MediaUrl{
Url: domain + wifiurl,
Quality: quality,
},
}
return result, err
}
func (p *QQMusicProvider) GetMediaLyric(meta miaosic.MetaData) ([]miaosic.Lyrics, error) {
resp, err := p.makeApiRequest("music.musichallSong.PlayLyricInfo", "GetPlayLyricInfo", map[string]any{
"songMid": meta.Identifier,
"crypt": 1,
"ct": 11,
"cv": 13020508,
"lrc_t": 0,
"qrc": 0,
"qrc_t": 0,
"roma": 1,
"roma_t": 0,
"trans": 1,
"trans_t": 0,
"type": 1,
})
if err != nil {
return nil, err
}
result := make([]miaosic.Lyrics, 0)
if lyricEnc := resp.Get("data.lyric").String(); lyricEnc != "" {
lyric, err := qrcDecrypt(lyricEnc)
if err == nil {
result = append(result, miaosic.ParseLyrics("default", lyric))
} else {
fmt.Println(err)
}
}
if lyricEnc := resp.Get("data.trans").String(); lyricEnc != "" {
lyric, err := qrcDecrypt(lyricEnc)
if err == nil {
result = append(result, miaosic.ParseLyrics("translation", lyric))
}
}
if lyricEnc := resp.Get("data.roma").String(); lyricEnc != "" {
lyric, err := qrcDecrypt(lyricEnc)
if err == nil {
result = append(result, miaosic.ParseLyrics("roma", lyric))
}
}
return result, nil
}

46
providers/qq/qq_test.go Normal file
View File

@@ -0,0 +1,46 @@
package qq
import (
"github.com/AynaLivePlayer/miaosic"
"github.com/k0kubun/pp/v3"
"github.com/stretchr/testify/require"
"testing"
)
var testApi *QQMusicProvider
func init() {
testApi = NewQQMusicProvider()
}
func TestQQ_MatchMedia(t *testing.T) {
result, ok := testApi.MatchMedia("002pCkT73uKyPL")
require.True(t, ok)
require.Equal(t, "002pCkT73uKyPL", result.Identifier)
require.Equal(t, testApi.GetName(), result.Provider)
}
func TestQQ_Search(t *testing.T) {
result, err := testApi.Search("还是会想你 h3R3", 1, 10)
require.NoError(t, err, "Search Error")
require.NotEmpty(t, result, "Search Result Empty")
require.Equal(t, 10, len(result), "Search Result Length")
//pp.Println(result)
}
func TestQQ_GetMediaInfo(t *testing.T) {
meta := miaosic.MetaData{Identifier: "002pCkT73uKyPL", Provider: testApi.GetName()}
result, err := testApi.GetMediaInfo(meta)
require.NoError(t, err, "GetMediaInfo Error")
require.NotEmpty(t, result, "GetMediaInfo Result Empty")
require.Equal(t, "还是会想你", result.Title)
pp.Println(result)
}
func TestQQ_GetMediaUrl(t *testing.T) {
meta := miaosic.MetaData{Identifier: "002pCkT73uKyPL", Provider: testApi.GetName()}
result, err := testApi.GetMediaUrl(meta, QualityOGG192)
require.NoError(t, err, "GetMediaUrl Error")
require.NotEmpty(t, result, "GetMediaUrl Result Empty")
t.Log(result)
}

49
providers/qq/quality.go Normal file
View File

@@ -0,0 +1,49 @@
package qq
import (
"github.com/AynaLivePlayer/miaosic"
"strings"
)
const (
QualityMaster = "AI00.flac" // MASTER: 臻品母带2.0,24Bit 192kHz,size_new[0]
QualityAtmos2 = "Q000.flac" // ATMOS_2: 臻品全景声2.0,16Bit 44.1kHz,size_new[1]
QualityAtmos51 = "Q001.flac" // ATMOS_51: 臻品音质2.0,16Bit 44.1kHz,size_new[2]
QualityFLAC = "F000.flac" // FLAC: flac 格式,16Bit 44.1kHz~24Bit 48kHz,size_flac
QualityOGG640 = "O801.ogg" // OGG_640: ogg 格式,640kbps,size_new[5]
QualityOGG320 = "O800.ogg" // OGG_320: ogg 格式,320kbps,size_new[3]
QualityOGG192 = "O600.ogg" // OGG_192: ogg 格式,192kbps,size_192ogg
QualityOGG96 = "O400.ogg" // OGG_96: ogg 格式,96kbps,size_96ogg
QualityMP3320 = "M800.mp3" // MP3_320: mp3 格式,320kbps,size_320mp3
QualityMP3128 = "M500.mp3" // MP3_128: mp3 格式,128kbps,size_128mp3
QualityACC192 = "C600.m4a" // ACC_192: m4a 格式,192kbps,size_192aac
QualityACC96 = "C400.m4a" // ACC_96: m4a 格式,96kbps,size_96aac
QualityACC48 = "C200.m4a" // ACC_48: m4a 格式,48kbps,size_48aac
)
const (
QualityEncMaster = "AIM0.mflac" // MASTER: 臻品母带2.0,24Bit 192kHz,size_new[0]
QualityEncAtmos2 = "Q0M0.mflac" // ATMOS_2: 臻品全景声2.0,16Bit 44.1kHz,size_new[1]
QualityEncAtmos51 = "Q0M1.mflac" // ATMOS_51: 臻品音质2.0,16Bit 44.1kHz,size_new[2]
QualityEncFLAC = "F0M0.mflac" // FLAC: mflac 格式,16Bit 44.1kHz~24Bit 48kHz,size_flac
QualityEncOGG640 = "O801.mgg" // OGG_640: mgg 格式,640kbps,size_new[5]
QualityEncOGG320 = "O800.mgg" // OGG_320: mgg 格式,320kbps,size_new[3]
QualityEncOGG192 = "O6M0.mgg" // OGG_192: mgg 格式,192kbps,size_192ogg
QualityEncOGG96 = "O4M0.mgg" // OGG_96: mgg 格式,96kbps,size_96ogg
)
func IsQqQuality(quality miaosic.Quality) bool {
val := strings.Split(string(quality), ".")
return len(val) == 2
}
func isEncryptedQuality(quality miaosic.Quality) bool {
val := strings.Split(string(quality), ".")
if len(val) != 2 {
return false
}
if val[1] == "mflac" || val[1] == "mgg" {
return true
}
return false
}

6
providers/qq/readme.md Normal file
View File

@@ -0,0 +1,6 @@
this part of code is inspired by
- https://github.com/luren-dc/QQMusicApi under MIT License
- https://github.com/fred913/goqrcdec under Apache License

117
providers/qq/request.go Normal file
View File

@@ -0,0 +1,117 @@
package qq
import (
"encoding/json"
"errors"
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/aynakeya/deepcolor/dphttp"
"github.com/tidwall/gjson"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
func (p *QQMusicProvider) makeApiRequest(module, method string, params map[string]interface{}) (gjson.Result, error) {
if !p.qimeiUpdated {
_, _ = getQimei(p.device, p.cfg.Version)
p.qimeiUpdated = true
}
expiredTime := time.UnixMilli(p.cred.ExpiredAt * 1000)
if expiredTime.Before(time.Now().Add(24*time.Hour)) && !p.tokenRefreshed {
_ = p.refreshToken()
// only refresh once
p.tokenRefreshed = true
}
// 公共参数
common := map[string]interface{}{
"ct": "11",
"tmeAppID": "qqmusic",
"format": "json",
"inCharset": "utf-8",
"outCharset": "utf-8",
"uid": "3931641530",
"cv": p.cfg.VersionCode,
"v": p.cfg.VersionCode,
"QIMEI36": p.device.Qimei,
}
cookie := map[string]interface{}{}
if p.cred.HasMusicKey() && p.cred.HasMusicID() {
common["authst"] = p.cred.MusicKey
common["qq"] = p.cred.MusicID
common["tmeLoginType"] = strconv.Itoa(p.cred.GetFormatedLoginType())
cookie["uin"] = p.cred.MusicID
cookie["qqmusic_key"] = p.cred.MusicKey
cookie["qm_keyst"] = p.cred.MusicKey
cookie["tmeLoginType"] = strconv.Itoa(p.cred.GetFormatedLoginType())
}
moduleKey := fmt.Sprintf("%s.%s", module, method)
requestData := map[string]interface{}{
"comm": common,
moduleKey: map[string]interface{}{
"module": module,
"method": method,
"param": params,
},
}
jsonData, _ := json.Marshal(requestData)
uri := p.cfg.Endpoint
if p.cfg.EnableSign {
// 创建请求
uri = p.cfg.EncEndpoint + "?sign=" + url.QueryEscape(qqSignStr(string(jsonData)))
}
request := dphttp.Request{
Method: http.MethodPost,
Url: dphttp.UrlMustParse(uri),
Header: map[string]string{
"Referer": "https://y.qq.com/",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54",
},
Data: jsonData,
}
cookieStr := ""
for k, v := range cookie {
cookieStr += fmt.Sprintf("%s=%s;", k, v)
}
if cookieStr != "" {
request.Header["Cookie"] = cookieStr
}
response, err := miaosic.Requester.HTTP(&request)
if err != nil {
return gjson.Result{}, err
}
jsonResp := gjson.ParseBytes(response.Body())
//fmt.Println(response.String())
moduleKeyEscaped := strings.ReplaceAll(moduleKey, ".", "\\.")
if !jsonResp.Get(moduleKeyEscaped).Exists() {
return gjson.Result{}, fmt.Errorf("miaosic (qq): api request fail")
}
code := jsonResp.Get(moduleKeyEscaped + ".code").Int()
if code == 4000 {
return jsonResp.Get(moduleKeyEscaped), errors.New("miaosic (qq): not login")
}
if code == 2000 {
return jsonResp.Get(moduleKeyEscaped), errors.New("miaosic (qq): invalid signature")
}
if code == 1000 {
return jsonResp.Get(moduleKeyEscaped), errors.New("miaosic (qq): invalid cookie")
}
if code != 0 {
return jsonResp.Get(moduleKeyEscaped), fmt.Errorf("miaosic (qq): invalid code: %d", code)
}
return jsonResp.Get(moduleKeyEscaped), nil
}

60
providers/qq/sign.go Normal file
View File

@@ -0,0 +1,60 @@
package qq
import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/json"
"strings"
)
// 签名算法实现
func qqSignMap(request map[string]interface{}) string {
data, _ := json.Marshal(request)
return qqSignStr(string(data))
}
func qqSignStr(data string) string {
hash := md5.Sum([]byte(data))
md5Str := strings.ToUpper(hex.EncodeToString(hash[:]))
// 头部处理
headPos := []int{21, 4, 9, 26, 16, 20, 27, 30}
head := make([]byte, len(headPos))
for i, pos := range headPos {
head[i] = md5Str[pos]
}
// 尾部处理
tailPos := []int{18, 11, 3, 2, 1, 7, 6, 25}
tail := make([]byte, len(tailPos))
for i, pos := range tailPos {
tail[i] = md5Str[pos]
}
// 中间部分处理
ol := []byte{212, 45, 80, 68, 195, 163, 163, 203, 157, 220, 254, 91, 204, 79, 104, 6}
hexMap := map[byte]byte{
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
'5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15,
}
middle := make([]byte, 16)
for i := 0; i < 32; i += 2 {
idx := i / 2
one := hexMap[md5Str[i]]
two := hexMap[md5Str[i+1]]
r := one*16 ^ two
middle[idx] = r ^ ol[idx]
}
// 组合签名
middleBase64 := base64.StdEncoding.EncodeToString(middle)
signature := "zzb" + string(head) + middleBase64 + string(tail)
signature = strings.ToLower(signature)
signature = strings.ReplaceAll(signature, "/", "")
signature = strings.ReplaceAll(signature, "+", "")
signature = strings.ReplaceAll(signature, "=", "")
return signature
}

11
providers/qq/sign_test.go Normal file
View File

@@ -0,0 +1,11 @@
package qq
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestQqSignStr(t *testing.T) {
data := "{\"module\":\"music.search.SearchCgiService\",\"method\":\"DoSearchForQQMusicMobile\",\"param\":{\"searchid\":\"xxx\",\"query\":\"asdfsadf\",\"search_type\":0,\"num_per_page\":10,\"page_num\":1,\"highlight\":1,\"grp\":1}}"
require.Equal(t, "zzb226c4cd6u6x73owgk9ltzzy8yktygb3187a9d", qqSignStr(data))
}

18
providers/qq/util_test.go Normal file
View File

@@ -0,0 +1,18 @@
package qq
import (
"fmt"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestHash33(t *testing.T) {
require.Equal(t, 1318936952, hash33("123fdas32asdf", 321))
}
func TestName(t *testing.T) {
if time.UnixMilli(1756987630 * 1000).Before(time.Now().Add(24 * time.Hour)) {
fmt.Println(123)
}
}

111
providers/qq/utils.go Normal file
View File

@@ -0,0 +1,111 @@
package qq
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
crand "crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"github.com/AynaLivePlayer/miaosic/providers/qq/goqrcdec"
"math/rand"
"strconv"
"time"
)
var rng *rand.Rand
func calcMd5(data ...string) string {
h := md5.New()
for _, d := range data {
_, _ = h.Write([]byte(d))
}
return hex.EncodeToString(h.Sum(nil))
}
func rsaEncrypt(plainText []byte, publicKeyPEM string) ([]byte, error) {
block, _ := pem.Decode([]byte(publicKeyPEM))
if block == nil {
return nil, fmt.Errorf("failed to parse PEM block containing the public key")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("key is not an RSA public key")
}
return rsa.EncryptPKCS1v15(crand.Reader, rsaPub, plainText)
}
func aesEncrypt(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// 填充数据
padding := aes.BlockSize - len(plaintext)%aes.BlockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
plaintext = append(plaintext, padText...)
// 使用key作为IV
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := key[:aes.BlockSize]
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
return ciphertext[aes.BlockSize:], nil
}
func getSearchID() string {
/* 随机 searchID
Returns:
随机 searchID
*/
e := rng.Intn(20) + 1
t := e * 18014398509481984
n := rng.Intn(4194305) * 4294967296
a := time.Now().UnixNano() / int64(time.Millisecond)
r := a % (24 * 60 * 60 * 1000)
return strconv.FormatInt(int64(t)+int64(n)+r, 10)
}
func getGuid() string {
const charset = "abcdef1234567890"
result := make([]byte, 32)
for i := range result {
result[i] = charset[rng.Intn(len(charset))]
}
return string(result)
}
// 计算hash33
func hash33(s string, h int) int {
val := uint64(h)
for _, c := range []byte(s) {
val = (val << 5) + val + uint64(c)
}
return int(2147483647 & val)
}
func qrcDecrypt(hexStr string) (string, error) {
// 1. hex 解码
data, err := hex.DecodeString(hexStr)
if err != nil {
return "", err
}
val, err := goqrcdec.DecodeQRC(data)
return string(val), err
}