mirror of
https://github.com/AynaLivePlayer/miaosic.git
synced 2025-12-11 07:18:14 +08:00
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:
40
providers/qq/credential.go
Normal file
40
providers/qq/credential.go
Normal 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
141
providers/qq/device.go
Normal 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
|
||||
}
|
||||
309
providers/qq/goqrcdec/goqrcdec.go
Normal file
309
providers/qq/goqrcdec/goqrcdec.go
Normal 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"
|
||||
|
||||
// --- Bit‐manipulation 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
12
providers/qq/init.go
Normal 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
108
providers/qq/login.go
Normal 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
274
providers/qq/login_qq.go
Normal 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
|
||||
}
|
||||
12
providers/qq/login_qq_test.go
Normal file
12
providers/qq/login_qq_test.go
Normal 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
89
providers/qq/playlist.go
Normal 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
|
||||
}
|
||||
27
providers/qq/playlist_test.go
Normal file
27
providers/qq/playlist_test.go
Normal 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
278
providers/qq/qimei.go
Normal 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
|
||||
}
|
||||
13
providers/qq/qimei_test.go
Normal file
13
providers/qq/qimei_test.go
Normal 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
258
providers/qq/qq.go
Normal 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
46
providers/qq/qq_test.go
Normal 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
49
providers/qq/quality.go
Normal 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
6
providers/qq/readme.md
Normal 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
117
providers/qq/request.go
Normal 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
60
providers/qq/sign.go
Normal 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
11
providers/qq/sign_test.go
Normal 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
18
providers/qq/util_test.go
Normal 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
111
providers/qq/utils.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user