将Spotify URI编码为Spotify Codes

25

Spotify Codes 是一种小型的条形码,可以用来分享歌曲、艺术家、用户、播放列表等信息。

它们通过不同高度的“条”来编码信息。有8个不同的高度可以应用到23个条上,这意味着有8^23种可能的条形码。

Spotify 根据它们的URI模式生成条形码。这个 URI spotify:playlist:37i9dQZF1DXcBWIGoYBM5M 映射到这个条形码:

Spotify code barcode

这个URI中包含了比条形码更多的信息(62^22)。你如何将URI映射到条形码?似乎不能直接对URI进行编码。更多背景信息,请参见我在这个问题的“答案”:https://dev59.com/QKbja4cB1Zd3GeqPkrmg#62120952

3个回答

18
专利说明了一般过程,这是我发现的。 这是一个更近期的专利 使用Spotify代码生成器时,网站向https://scannables.scdn.co/uri/plain/[format]/[background-color-in-hex]/[code-color-in-text]/[size]/[spotify-URI]发出请求。
使用Burp Suite扫描Spotify中的代码时,应用程序会向Spotify的API发送请求:https://spclient.wg.spotify.com/scannable-id/id/[CODE]?format=json,其中[CODE]是您要查找的媒体引用。可以通过python进行此请求,但仅能使用应用程序生成的[TOKEN],因为这是获取正确范围的唯一方式。应用程序令牌在大约半个小时后过期。
import requests

head={
"X-Client-Id": "58bd3c95768941ea9eb4350aaa033eb3",
"Accept-Encoding": "gzip, deflate",
"Connection": "close",
"App-Platform": "iOS",
"Accept": "*/*",
"User-Agent": "Spotify/8.5.68 iOS/13.4 (iPhone9,3)",
"Accept-Language": "en",
"Authorization": "Bearer [TOKEN]", 
"Spotify-App-Version": "8.5.68"}

response = requests.get('https://spclient.wg.spotify.com:443/scannable-id/id/26560102031?format=json', headers=head)

print(response)
print(response.json())

这将返回:

<Response [200]>
{'target': 'spotify:playlist:37i9dQZF1DXcBWIGoYBM5M'}

所以26560102031是您的播放列表的媒体参考。

该专利声明称,代码首先被检测,然后可能使用Gray表将其转换为63位。例如,361354354471425226605被编码为010 101 001 010 111 110 010 111 110 110 100 001 110 011 111 011 011 101 101 000 111。

但是发送到API的代码是6875667268,我不确定媒体参考是如何生成的,但这是查找表中使用的数字。

参考文献包含0-9的整数,与0-7的灰色表进行比较,这意味着使用了正常二进制的算法。该专利谈到使用卷积码,然后使用Viterbi算法进行错误校正,因此这可能是该算法的输出。我认为如果没有状态,这是不可能重新创建的东西。但是,如果您能更好地解释专利,我会很感兴趣。

这个媒体参考有10个数字,但其他参考文献有11个或12个。

这里有两个原始距离的示例,灰度表二进制和媒体参考:

1.

022673352171662032460

000 011 011 101 100 010 010 111 011 001 100 001 101 101 011 000 010 011 110 101 000

67775490487

2. 574146602473467556050

111 100 110 001 110 101 101 000 011 110 100 010 110 101 100 111 111 101 000 111 000

57639171874

编辑:

一些额外的信息: 有些帖子描述了如何将任何文本编码成代码,例如spotify:playlist:HelloWorld,但这种方法已经不再起作用。

我还通过代理发现,您可以使用该域来获取上面代码的曲目专辑封面。这表明Spotify的API和这个可扫描的网址之间的集成比以前想象的更为密切。因为它不仅存储URI及其代码,而且还可以验证URI并返回更新的专辑封面。

https://scannables.scdn.co/uri/800/spotify%3Atrack%3A0J8oh5MAMyUPRIgflnjwmB


谢谢您提供这些非常有用的信息。我有一些关于您得到的值的问题。第一个媒体引用(26560102031)返回给我的是 spotify:track:1ykrctzPhcSS9GS3aHdtMt,而不是播放列表。另外两个媒体引用返回的是 spotify:user:jimmylavallin:playlist:2hXLRTDrNa4rG1XyM0ngT1spotify:user:spotify:playlist:37i9dQZF1DWZq91oLsHZvy。这是您得到的吗? - Peter Boone
1
啊,看起来我刚刚复制了错误的代码。你问题中 Spotify 代码的媒体引用是 57268659651,而其他两个是正确的,只是随机播放列表。我尝试了很长时间将距离转换为媒体引用,但没有成功。 - Archie Webster
太酷了,谢谢!我正在研究它,但我觉得我们可能做不到。如果我发现什么,我会告诉你的。 - Peter Boone
1
阿奇,我在这里写了一篇关于这些代码的文章:https://boonepeter.github.io/posts/2020-11-10-spotify-codes/。 - Peter Boone

6
你的怀疑是正确的 - 他们正在使用查找表。关于所有有趣的技术细节,相关专利可以在这里找到。

哇,不错的发现! - Peter Boone

5
非常有趣的讨论。一直被条形码所吸引,所以我不得不看一下。我仅分析了条形码本身(没有使用媒体参考的API),并认为我已经理解了基本的编码过程。然而,根据以上两个示例,我并不确定我对媒体参考到37位向量的映射是否正确(即它适用于第2种情况但不适用于第1种情况)。无论如何,如果你有更多的配对数据,最后一个问题就应该很容易解决了。请告诉我。
对于那些想要弄清楚这个问题的人,请不要阅读下面的剧透!
事实证明,专利中概述的基本过程是正确的,但缺少细节。我会使用上面的示例进行总结。我实际上是倒着分析的,这就是为什么我认为代码描述基本上是正确的,除了步骤(1),即我生成了45个条形码,并且所有匹配的都有这个代码。
1. Map the media reference as integer to 37 bit vector. 
Something like write number in base 2, with lowest significant bit 
on the left and zero-padding on right if necessary. 
   57639171874 -> 0100010011101111111100011101011010110

2. Calculate CRC-8-CCITT, i.e. generator x^8 + x^2 + x + 1
   The following steps are needed to calculate the 8 CRC bits:

   Pad with 3 bits on the right:
   01000100 11101111 11110001 11010110 10110000
   Reverse bytes:
   00100010 11110111 10001111 01101011 00001101
   Calculate CRC as normal (highest order degree on the left):
   -> 11001100
   Reverse CRC:
   -> 00110011
   Invert check:
   -> 11001100
   Finally append to step 1 result:
   01000100 11101111 11110001 11010110 10110110 01100

3. Convolutionally encode the 45 bits using the common generator
polynomials (1011011, 1111001) in binary with puncture pattern 
110110 (or 101, 110 on each stream). The result of step 2 is 
encoded using tail-biting, meaning we begin the shift register 
in the state of the last 6 bits of the 45 long input vector. 

  Prepend stream with last 6 bits of data:
  001100 01000100 11101111 11110001 11010110 10110110 01100
  Encode using first generator:
  (a) 100011100111110100110011110100000010001001011
  Encode using 2nd generator:
  (b) 110011100010110110110100101101011100110011011
  Interleave bits (abab...):
  11010000111111000010111011110011010011110001...
  1010111001110001000101011000010110000111001111
  Puncture every third bit:
  111000111100101111101110111001011100110000100100011100110011

4. Permute data by choosing indices 0, 7, 14, 21, 28, 35, 42, 49, 
56, 3, 10..., i.e. incrementing 7 modulo 60. (Note: unpermute by 
incrementing 43 mod 60).

  The encoded sequence after permuting is
  111100110001110101101000011110010110101100111111101000111000

5. The final step is to map back to bar lengths 0 to 7 using the
gray map (000,001,011,010,110,111,101,100). This gives the 20 bar 
encoding. As noted before, add three bars: short one on each end 
and a long one in the middle. 

更新:我添加了一个条形码(levels)解码器(假设没有错误),并且添加了一个备用编码器,该编码器遵循上述描述而不是等效的线性代数方法。希望这样更清晰明了。

更新2:删除了大部分硬编码数组以说明它们是如何生成的。

线性代数方法定义了线性变换(spotify_generator)和掩码,以将37位输入映射到60位卷积编码数据。掩码是8位反转CRC进行卷积编码的结果。spotify_generator是一个37x60的矩阵,实现了CRC(一个37x45矩阵)和卷积码(一个45x60矩阵)的生成器的乘积。您可以通过将该函数应用于适当大小的生成器矩阵的每一行来从编码函数创建生成器矩阵。例如,将向每个37位数据向量添加8位的CRC函数应用于37x37单位矩阵的每一行。

import numpy as np
import crccheck


# Utils for conversion between int, array of binary
# and array of bytes (as ints)
def int_to_bin(num, length, endian):
    if endian == 'l':
        return [num >> i & 1 for i in range(0, length)]
    elif endian == 'b':
        return [num >> i & 1 for i in range(length-1, -1, -1)]

def bin_to_int(bin,length):
    return int("".join([str(bin[i]) for i in range(length-1,-1,-1)]),2)

def bin_to_bytes(bin, length):
    b = bin[0:length] + [0] * (-length % 8)
    return [(b[i]<<7) + (b[i+1]<<6) + (b[i+2]<<5) + (b[i+3]<<4) + 
        (b[i+4]<<3) + (b[i+5]<<2) + (b[i+6]<<1) + b[i+7] for i in range(0,len(b),8)]
    
# Return the circular right shift of an array by 'n' positions    
def shift_right(arr, n):
    return arr[-n % len(arr):len(arr):] + arr[0:-n % len(arr)]

gray_code = [0,1,3,2,7,6,4,5]
gray_code_inv = [[0,0,0],[0,0,1],[0,1,1],[0,1,0],
                 [1,1,0],[1,1,1],[1,0,1],[1,0,0]]

# CRC using Rocksoft model: 
# NOTE: this is not quite any of their predefined CRC's
# 8: number of check bits (degree of poly)
# 0x7: representation of poly without high term (x^8+x^2+x+1)
# 0x0: initial fill of register
# True: byte reverse data
# True: byte reverse check
# 0xff: Mask check (i.e. invert)
spotify_crc = crccheck.crc.Crc(8, 0x7, 0x0, True, True, 0xff)

def calc_spotify_crc(bin37):
    bytes = bin_to_bytes(bin37, 37)
    return int_to_bin(spotify_crc.calc(bytes), 8, 'b')

def check_spotify_crc(bin45):
    data = bin_to_bytes(bin45,37)
    return spotify_crc.calc(data) == bin_to_bytes(bin45[37:], 8)[0]

# Simple convolutional encoder
def encode_cc(dat):
    gen1 = [1,0,1,1,0,1,1]
    gen2 = [1,1,1,1,0,0,1]
    punct = [1,1,0]
    dat_pad = dat[-6:] + dat # 6 bits are needed to initialize
                             # register for tail-biting
    stream1 = np.convolve(dat_pad, gen1, mode='valid') % 2
    stream2 = np.convolve(dat_pad, gen2, mode='valid') % 2
    enc = [val for pair in zip(stream1, stream2) for val in pair]
    return [enc[i] for i in range(len(enc)) if punct[i % 3]]
    
# To create a generator matrix for a code, we encode each row
# of the identity matrix. Note that the CRC is not quite linear
# because of the check mask so we apply the lamda function to
# invert it. Given a 37 bit media reference we can encode by
#     ref * spotify_generator + spotify_mask (mod 2)
_i37 = np.identity(37, dtype=bool)
crc_generator = [_i37[r].tolist() + 
          list(map(lambda x : 1-x, calc_spotify_crc(_i37[r].tolist())))
          for r in range(37)]
spotify_generator = 1*np.array([encode_cc(crc_generator[r]) for r in range(37)], dtype=bool)  
del _i37

spotify_mask = 1*np.array(encode_cc(37*[0] + 8*[1]), dtype=bool) 
    
# The following matrix is used to "invert" the convolutional code.
# In particular, we choose a 45 vector basis for the columns of the
# generator matrix (by deleting those in positions equal to 2 mod 4)
# and then inverting the matrix. By selecting the corresponding 45 
# elements of the convolutionally encoded vector and multiplying 
# on the right by this matrix, we get back to the unencoded data,
# assuming there are no errors.
# Note: numpy does not invert binary matrices, i.e. GF(2), so we
# hard code the following 3 row vectors to generate the matrix.
conv_gen = [[0,1,0,1,1,1,1,0,1,1,0,0,0,1]+31*[0],
            [1,0,1,0,1,0,1,0,0,0,1,1,1] + 32*[0],
            [0,0,1,0,1,1,1,1,1,1,0,0,1] + 32*[0] ]

conv_generator_inv = 1*np.array([shift_right(conv_gen[(s-27) % 3],s) for s in range(27,72)], dtype=bool) 


# Given an integer media reference, returns list of 20 barcode levels
def spotify_bar_code(ref):
    bin37 = np.array([int_to_bin(ref, 37, 'l')], dtype=bool)
    enc = (np.add(1*np.dot(bin37, spotify_generator), spotify_mask) % 2).flatten()
    perm = [enc[7*i % 60] for i in range(60)]
    return [gray_code[4*perm[i]+2*perm[i+1]+perm[i+2]] for i in range(0,len(perm),3)]
    
# Equivalent function but using CRC and CC encoders.
def spotify_bar_code2(ref):
    bin37 = int_to_bin(ref, 37, 'l')
    enc_crc = bin37 + calc_spotify_crc(bin37)
    enc_cc = encode_cc(enc_crc)
    perm = [enc_cc[7*i % 60] for i in range(60)]
    return [gray_code[4*perm[i]+2*perm[i+1]+perm[i+2]] for i in range(0,len(perm),3)]
    
# Given 20 (clean) barcode levels, returns media reference
def spotify_bar_decode(levels):
    level_bits = np.array([gray_code_inv[levels[i]] for i in range(20)], dtype=bool).flatten()
    conv_bits = [level_bits[43*i % 60] for i in range(60)]
    cols = [i for i in range(60) if i % 4 != 2] # columns to invert
    conv_bits45 = np.array([conv_bits[c] for c in cols], dtype=bool)
    bin45 = (1*np.dot(conv_bits45, conv_generator_inv) % 2).tolist()
    if check_spotify_crc(bin45):
        return bin_to_int(bin45, 37)
    else:
        print('Error in levels; Use real decoder!!!')
        return -1

一个例子:

>>> levels = [5,7,4,1,4,6,6,0,2,4,3,4,6,7,5,5,6,0,5,0]
>>> spotify_bar_decode(levels)
57639171874
>>> spotify_barcode(57639171874)
[5, 7, 4, 1, 4, 6, 6, 0, 2, 4, 3, 4, 6, 7, 5, 5, 6, 0, 5, 0]

我想我应该提一下,要从条码长度回溯到媒体参考,我们确实需要应用解码器来纠正条形码长度。但是为了快速而简单,我们可以通过将其乘以奇偶校验矩阵来验证条码长度是否正确(即形成一个没有错误的正确码字),如果是,则只需应用类似的线性变换来“撤销”编码。 - Doyle
你关于编码的回答非常准确!我使用上面提到的媒体参考获取了Spotify代码,并将它们与您的编码进行了核对,发现它们是匹配的。请问你是如何生成spotify_generator_compact的?并且你能演示一下你之前评论中提到的反向过程吗?我们假设不需要进行任何纠错操作。 - Peter Boone
哦,太好了,它在你所有的例子中都起作用了吗?我有点困惑为什么它没有匹配上面的第一个例子。 - Doyle
1
我会在接下来的几天内更新代码以进行“伪”解码。我很乐意向您发送更详细的PDF文件,介绍我如何完成线性代数步骤。我非常喜欢您在其他页面上的写作。 - Doyle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接