mirror of
https://github.com/owntone/owntone-server.git
synced 2025-03-13 21:12:56 -04:00
[spotify] Import librespot-c fixes and hashcash support
This commit is contained in:
parent
032c564b5a
commit
7d4d5e98b3
@ -37,9 +37,13 @@ struct sp_metadata
|
||||
size_t file_len;
|
||||
};
|
||||
|
||||
// How to identify towards Spotify. The device_id can be set to an actual value
|
||||
// identifying the client, but the rest are unfortunately best left as zeroes,
|
||||
// which will make librespot-c use defaults that spoof whitelisted clients.
|
||||
struct sp_sysinfo
|
||||
{
|
||||
char client_name[16];
|
||||
char client_id[33];
|
||||
char client_version[16];
|
||||
char client_build_id[16];
|
||||
char device_id[41]; // librespot gives a 20 byte id (so 40 char hex + 1 zero term)
|
||||
|
@ -919,6 +919,55 @@ handle_clienttoken(struct sp_message *msg, struct sp_session *session)
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
hashcash_challenges_free(struct crypto_hashcash_challenge **challenges, int *n_challenges)
|
||||
{
|
||||
for (int i = 0; i < *n_challenges; i++)
|
||||
free(challenges[i]->ctx);
|
||||
|
||||
free(*challenges);
|
||||
*challenges = NULL;
|
||||
*n_challenges = 0;
|
||||
}
|
||||
|
||||
static enum sp_error
|
||||
handle_login5_challenges(Spotify__Login5__V3__Challenges *challenges, uint8_t *login_ctx, size_t login_ctx_len, struct sp_session *session)
|
||||
{
|
||||
Spotify__Login5__V3__Challenge *this_challenge;
|
||||
struct crypto_hashcash_challenge *crypto_challenge;
|
||||
int ret;
|
||||
int i;
|
||||
|
||||
session->n_hashcash_challenges = challenges->n_challenges;
|
||||
session->hashcash_challenges = calloc(challenges->n_challenges, sizeof(struct crypto_hashcash_challenge));
|
||||
|
||||
for (i = 0, crypto_challenge = session->hashcash_challenges; i < session->n_hashcash_challenges; i++, crypto_challenge++)
|
||||
{
|
||||
this_challenge = challenges->challenges[i];
|
||||
|
||||
if (this_challenge->challenge_case != SPOTIFY__LOGIN5__V3__CHALLENGE__CHALLENGE_HASHCASH)
|
||||
RETURN_ERROR(SP_ERR_INVALID, "Received unsupported login5 challenge");
|
||||
|
||||
if (this_challenge->hashcash->prefix.len != sizeof(crypto_challenge->prefix))
|
||||
RETURN_ERROR(SP_ERR_INVALID, "Received hashcash challenge with unexpected prefix length");
|
||||
|
||||
crypto_challenge->ctx_len = login_ctx_len;
|
||||
crypto_challenge->ctx = malloc(login_ctx_len);
|
||||
memcpy(crypto_challenge->ctx, login_ctx, login_ctx_len);
|
||||
|
||||
memcpy(crypto_challenge->prefix, this_challenge->hashcash->prefix.data, sizeof(crypto_challenge->prefix));
|
||||
crypto_challenge->wanted_zero_bits = this_challenge->hashcash->length;
|
||||
crypto_challenge->max_iterations = 100000; //TODO define or make variable
|
||||
|
||||
}
|
||||
|
||||
return SP_OK_DONE;
|
||||
|
||||
error:
|
||||
hashcash_challenges_free(&session->hashcash_challenges, &session->n_hashcash_challenges);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static enum sp_error
|
||||
handle_login5(struct sp_message *msg, struct sp_session *session)
|
||||
{
|
||||
@ -950,7 +999,9 @@ handle_login5(struct sp_message *msg, struct sp_session *session)
|
||||
break;
|
||||
case SPOTIFY__LOGIN5__V3__LOGIN_RESPONSE__RESPONSE_CHALLENGES:
|
||||
sp_cb.logmsg("Login %zu challenges\n", response->challenges->n_challenges);
|
||||
// TODO support hashcash challenges
|
||||
ret = handle_login5_challenges(response->challenges, response->login_context.data, response->login_context.len, session);
|
||||
if (ret != SP_OK_DONE)
|
||||
goto error;
|
||||
break;
|
||||
case SPOTIFY__LOGIN5__V3__LOGIN_RESPONSE__RESPONSE_ERROR:
|
||||
RETURN_ERROR(SP_ERR_LOGINFAILED, err2txt(response->error, sp_login5_error_map, ARRAY_SIZE(sp_login5_error_map)));
|
||||
@ -1728,7 +1779,7 @@ msg_make_clienttoken(struct sp_message *msg, struct sp_session *session)
|
||||
dreq.connectivity_sdk_data = &sdk_data;
|
||||
dreq.data_case = SPOTIFY__CLIENTTOKEN__HTTP__V0__CLIENT_DATA_REQUEST__DATA_CONNECTIVITY_SDK_DATA;
|
||||
dreq.client_version = sp_sysinfo.client_version; // e.g. "0.0.0" (SpotifyLikeClient)
|
||||
dreq.client_id = SP_CLIENT_ID_HEX;
|
||||
dreq.client_id = sp_sysinfo.client_id;
|
||||
|
||||
treq.client_data = &dreq;
|
||||
treq.request_type = SPOTIFY__CLIENTTOKEN__HTTP__V0__CLIENT_TOKEN_REQUEST_TYPE__REQUEST_CLIENT_DATA_REQUEST;
|
||||
@ -1747,17 +1798,87 @@ msg_make_clienttoken(struct sp_message *msg, struct sp_session *session)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
challenge_solutions_free(Spotify__Login5__V3__ChallengeSolutions *solutions)
|
||||
{
|
||||
Spotify__Login5__V3__ChallengeSolution *this_solution;
|
||||
int i;
|
||||
|
||||
if (!solutions)
|
||||
return;
|
||||
|
||||
for (i = 0; i < solutions->n_solutions; i++)
|
||||
{
|
||||
this_solution = solutions->solutions[i];
|
||||
|
||||
free(this_solution->hashcash->duration);
|
||||
free(this_solution->hashcash->suffix.data);
|
||||
free(this_solution->hashcash);
|
||||
}
|
||||
|
||||
free(solutions->solutions);
|
||||
}
|
||||
|
||||
// Finds solutions to the challenges stored in *challenges and adds them to *solutions
|
||||
static int
|
||||
challenge_solutions_append(Spotify__Login5__V3__ChallengeSolutions *solutions, struct crypto_hashcash_challenge *challenges, int n_challenges)
|
||||
{
|
||||
Spotify__Login5__V3__ChallengeSolution *this_solution;
|
||||
struct crypto_hashcash_challenge *crypto_challenge;
|
||||
struct crypto_hashcash_solution crypto_solution;
|
||||
size_t suffix_len = sizeof(crypto_solution.suffix);
|
||||
int ret;
|
||||
int i;
|
||||
|
||||
solutions->n_solutions = n_challenges;
|
||||
solutions->solutions = calloc(n_challenges, sizeof(Spotify__Login5__V3__ChallengeSolution));
|
||||
|
||||
for (i = 0, crypto_challenge = challenges; i < n_challenges; i++, crypto_challenge++)
|
||||
{
|
||||
ret = crypto_hashcash_solve(&crypto_solution, crypto_challenge, &sp_errmsg);
|
||||
if (ret < 0)
|
||||
RETURN_ERROR(SP_ERR_INVALID, sp_errmsg);
|
||||
|
||||
this_solution = solutions->solutions[i];
|
||||
spotify__login5__v3__challenge_solution__init(this_solution);
|
||||
this_solution->solution_case = SPOTIFY__LOGIN5__V3__CHALLENGE_SOLUTION__SOLUTION_HASHCASH;
|
||||
|
||||
this_solution->hashcash = malloc(sizeof(Spotify__Login5__V3__Challenges__HashcashSolution));
|
||||
spotify__login5__v3__challenges__hashcash_solution__init(this_solution->hashcash);
|
||||
|
||||
this_solution->hashcash->duration = malloc(sizeof(Google__Protobuf__Duration));
|
||||
google__protobuf__duration__init(this_solution->hashcash->duration);
|
||||
|
||||
this_solution->hashcash->suffix.len = suffix_len;
|
||||
this_solution->hashcash->suffix.data = malloc(suffix_len);
|
||||
memcpy(this_solution->hashcash->suffix.data, crypto_solution.suffix, suffix_len);
|
||||
|
||||
this_solution->hashcash->duration->seconds = crypto_solution.duration.tv_sec;
|
||||
this_solution->hashcash->duration->nanos = crypto_solution.duration.tv_nsec;;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
challenge_solutions_free(solutions);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Ref. login5/login5.go
|
||||
static int
|
||||
msg_make_login5(struct sp_message *msg, struct sp_session *session)
|
||||
{
|
||||
struct http_request *hreq = &msg->payload.hreq;
|
||||
Spotify__Login5__V3__LoginRequest req = SPOTIFY__LOGIN5__V3__LOGIN_REQUEST__INIT;
|
||||
Spotify__Login5__V3__ChallengeSolutions solutions = SPOTIFY__LOGIN5__V3__CHALLENGE_SOLUTIONS__INIT;
|
||||
Spotify__Login5__V3__ClientInfo client_info = SPOTIFY__LOGIN5__V3__CLIENT_INFO__INIT;
|
||||
Spotify__Login5__V3__Credentials__StoredCredential stored_credential = SPOTIFY__LOGIN5__V3__CREDENTIALS__STORED_CREDENTIAL__INIT;
|
||||
struct sp_token *token = &session->http_accesstoken;
|
||||
uint8_t *login_context = NULL;
|
||||
size_t login_context_len;
|
||||
time_t now = time(NULL);
|
||||
bool must_refresh;
|
||||
int ret;
|
||||
|
||||
must_refresh = (now > token->received_ts + token->expires_after_seconds);
|
||||
if (!must_refresh)
|
||||
@ -1766,7 +1887,25 @@ msg_make_login5(struct sp_message *msg, struct sp_session *session)
|
||||
if (session->credentials.stored_cred_len == 0)
|
||||
return -1;
|
||||
|
||||
client_info.client_id = SP_CLIENT_ID_HEX;
|
||||
// This is our second login5 request - Spotify returned challenges after the first.
|
||||
// The login_context is echoed from Spotify's response to the first login5.
|
||||
if (session->hashcash_challenges)
|
||||
{
|
||||
login_context_len = session->hashcash_challenges->ctx_len;
|
||||
login_context = malloc(login_context_len);
|
||||
memcpy(login_context, session->hashcash_challenges->ctx, login_context_len);
|
||||
|
||||
ret = challenge_solutions_append(&solutions, session->hashcash_challenges, session->n_hashcash_challenges);
|
||||
hashcash_challenges_free(&session->hashcash_challenges, &session->n_hashcash_challenges);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
req.challenge_solutions = &solutions;
|
||||
req.login_context.data = login_context;
|
||||
req.login_context.len = login_context_len;
|
||||
}
|
||||
|
||||
client_info.client_id = sp_sysinfo.client_id;
|
||||
client_info.device_id = sp_sysinfo.device_id;
|
||||
|
||||
req.client_info = &client_info;
|
||||
@ -1789,7 +1928,25 @@ msg_make_login5(struct sp_message *msg, struct sp_session *session)
|
||||
hreq->headers[1] = asprintf_or_die("Content-Type: application/x-protobuf");
|
||||
hreq->headers[2] = asprintf_or_die("Client-Token: %s", session->http_clienttoken.value);
|
||||
|
||||
challenge_solutions_free(&solutions);
|
||||
free(login_context);
|
||||
return 0;
|
||||
|
||||
error:
|
||||
challenge_solutions_free(&solutions);
|
||||
free(login_context);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
msg_make_login5_challenges(struct sp_message *msg, struct sp_session *session)
|
||||
{
|
||||
// Spotify didn't give us any challenges during login5, so we can just proceed
|
||||
if (!session->hashcash_challenges)
|
||||
return 1; // Continue to next message
|
||||
|
||||
// Otherwise make another login5 request that includes the challenge responses
|
||||
return msg_make_login5(msg, session);
|
||||
}
|
||||
|
||||
// Ref. spclient/spclient.go
|
||||
@ -1893,6 +2050,7 @@ static struct sp_seq_request seq_requests[][7] =
|
||||
// The first two will be skipped if valid tokens already exist
|
||||
{ SP_SEQ_MEDIA_OPEN, "CLIENTTOKEN", SP_PROTO_HTTP, msg_make_clienttoken, NULL, handle_clienttoken, },
|
||||
{ SP_SEQ_MEDIA_OPEN, "LOGIN5", SP_PROTO_HTTP, msg_make_login5, NULL, handle_login5, },
|
||||
{ SP_SEQ_MEDIA_OPEN, "LOGIN5_CHALLENGES", SP_PROTO_HTTP, msg_make_login5_challenges, NULL, handle_login5, },
|
||||
{ SP_SEQ_MEDIA_OPEN, "METADATA_GET", SP_PROTO_HTTP, msg_make_metadata_get, NULL, handle_metadata_get, },
|
||||
{ SP_SEQ_MEDIA_OPEN, "AUDIO_KEY_GET", SP_PROTO_TCP, msg_make_audio_key_get, prepare_tcp, handle_tcp_generic, },
|
||||
{ SP_SEQ_MEDIA_OPEN, "STORAGE_RESOLVE", SP_PROTO_HTTP, msg_make_storage_resolve, NULL, handle_storage_resolve, },
|
||||
|
@ -13,6 +13,7 @@
|
||||
/* ----------------------------------- Crypto ------------------------------- */
|
||||
|
||||
#define SHA512_DIGEST_LENGTH 64
|
||||
#define SHA1_DIGEST_LENGTH 20
|
||||
#define bnum_new(bn) \
|
||||
do { \
|
||||
if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) { \
|
||||
@ -462,3 +463,129 @@ crypto_base62_to_bin(uint8_t *out, size_t out_len, const char *in)
|
||||
bnum_free(base);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
count_trailing_zero_bits(uint8_t *data, size_t data_len)
|
||||
{
|
||||
int zero_bits = 0;
|
||||
size_t idx;
|
||||
int bit;
|
||||
|
||||
for (idx = data_len - 1; idx >= 0; idx--)
|
||||
{
|
||||
for (bit = 0; bit < 8; bit++)
|
||||
{
|
||||
if (data[idx] & (1 << bit))
|
||||
return zero_bits;
|
||||
|
||||
zero_bits++;
|
||||
}
|
||||
}
|
||||
|
||||
return zero_bits;
|
||||
}
|
||||
|
||||
static void
|
||||
sha1_sum(uint8_t *digest, uint8_t *data, size_t data_len, gcry_md_hd_t hdl)
|
||||
{
|
||||
gcry_md_reset(hdl);
|
||||
|
||||
gcry_md_write(hdl, data, data_len);
|
||||
gcry_md_final(hdl);
|
||||
|
||||
memcpy(digest, gcry_md_read(hdl, GCRY_MD_SHA1), SHA1_DIGEST_LENGTH);
|
||||
}
|
||||
|
||||
static void
|
||||
sha1_two_part_sum(uint8_t *digest, uint8_t *data1, size_t data1_len, uint8_t *data2, size_t data2_len, gcry_md_hd_t hdl)
|
||||
{
|
||||
gcry_md_reset(hdl);
|
||||
|
||||
gcry_md_write(hdl, data1, data1_len);
|
||||
gcry_md_write(hdl, data2, data2_len);
|
||||
gcry_md_final(hdl);
|
||||
|
||||
memcpy(digest, gcry_md_read(hdl, GCRY_MD_SHA1), SHA1_DIGEST_LENGTH);
|
||||
}
|
||||
|
||||
static inline void
|
||||
increase_hashcash(uint8_t *data, int idx)
|
||||
{
|
||||
while (++data[idx] == 0 && idx > 0)
|
||||
idx--;
|
||||
}
|
||||
|
||||
static void
|
||||
timespec_sub(struct timespec *a, struct timespec *b, struct timespec *result)
|
||||
{
|
||||
result->tv_sec = a->tv_sec - b->tv_sec;
|
||||
result->tv_nsec = a->tv_nsec - b->tv_nsec;
|
||||
if (result->tv_nsec < 0)
|
||||
{
|
||||
--result->tv_sec;
|
||||
result->tv_nsec += 1000000000L;
|
||||
}
|
||||
}
|
||||
|
||||
// Example challenge:
|
||||
// - loginctx 0300c798435c4b0beb91e3b1db591d0a7f2e32816744a007af41cc7c8043b9295e1ed8a13cc323e4af2d0a3c42463b7a358ed116c33695989e0bfade0dab9c6bc6f7f928df5d49069e8ca4c04c34034669fc97e93da1ca17a7c11b2ffbb9b85f2265b10f6c83f7ef672240cb535eb122265da9b6f8d1a55af522fcbb40efc4eb753756ea38a63aff95d3228219afb0ab887075ac2fe941f7920fd19d32226052fe0956c71f0cb63ba702dd72d50d769920cd99ec6a45e00c85af5287b5d0031d6be4072efe71c59dffa5baa4077cd2eab4f22143eff18c31c69b8647e7f517468c84ed9548943fb1ba6b750ef63cdf9ce0a0fd07cb22d19484f4baa8ee6fa35fc573d9
|
||||
// - prefix 48859603d6c16c3202292df155501c55
|
||||
// - length (difficulty) 10
|
||||
// Solution:
|
||||
// - suffix 7f7e558bd10c37d200000000000002c7
|
||||
int
|
||||
crypto_hashcash_solve(struct crypto_hashcash_solution *solution, struct crypto_hashcash_challenge *challenge, const char **errmsg)
|
||||
{
|
||||
gcry_md_hd_t hdl;
|
||||
struct timespec start_ts;
|
||||
struct timespec stop_ts;
|
||||
uint8_t digest[SHA1_DIGEST_LENGTH];
|
||||
bool solution_found = false;
|
||||
int i;
|
||||
|
||||
// 1. Hash loginctx
|
||||
// 2. Create a 16 byte suffix, fill first 8 bytes with last 8 bytes of hash, last with zeroes
|
||||
// 3. Hash challenge prefix + suffix
|
||||
// 4. Check if X last bits of hash is zeroes, where X is challenge length
|
||||
// 5. If not, increment both 8-byte parts of suffix and goto 3
|
||||
|
||||
memset(solution, 0, sizeof(struct crypto_hashcash_solution));
|
||||
|
||||
if (gcry_md_open(&hdl, GCRY_MD_SHA1, 0) != GPG_ERR_NO_ERROR)
|
||||
{
|
||||
*errmsg = "Error initialising SHA1 hasher";
|
||||
return -1;
|
||||
}
|
||||
|
||||
sha1_sum(digest, challenge->ctx, challenge->ctx_len, hdl);
|
||||
|
||||
memcpy(solution->suffix, digest + SHA1_DIGEST_LENGTH - 8, 8);
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &start_ts);
|
||||
|
||||
for (i = 0; i < challenge->max_iterations; i++)
|
||||
{
|
||||
sha1_two_part_sum(digest, challenge->prefix, sizeof(challenge->prefix), solution->suffix, sizeof(solution->suffix), hdl);
|
||||
|
||||
solution_found = (count_trailing_zero_bits(digest, SHA1_DIGEST_LENGTH) >= challenge->wanted_zero_bits);
|
||||
if (solution_found)
|
||||
break;
|
||||
|
||||
increase_hashcash(solution->suffix, 7);
|
||||
increase_hashcash(solution->suffix + 8, 7);
|
||||
}
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &stop_ts);
|
||||
|
||||
timespec_sub(&stop_ts, &start_ts, &solution->duration);
|
||||
|
||||
gcry_md_close(hdl);
|
||||
|
||||
if (!solution_found)
|
||||
{
|
||||
*errmsg = "Could not find a hashcash solution";
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include <inttypes.h>
|
||||
#include <stddef.h>
|
||||
#include <gcrypt.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "shannon/Shannon.h"
|
||||
|
||||
@ -33,6 +34,26 @@ struct crypto_keys
|
||||
size_t shared_secret_len;
|
||||
};
|
||||
|
||||
struct crypto_hashcash_challenge
|
||||
{
|
||||
uint8_t *ctx;
|
||||
size_t ctx_len;
|
||||
uint8_t prefix[16];
|
||||
|
||||
// Required number of trailing zero bits in the SHA1 of prefix and suffix.
|
||||
// More bits -> more difficult.
|
||||
int wanted_zero_bits;
|
||||
|
||||
// Give up limit
|
||||
int max_iterations;
|
||||
};
|
||||
|
||||
struct crypto_hashcash_solution
|
||||
{
|
||||
uint8_t suffix[16];
|
||||
struct timespec duration;
|
||||
};
|
||||
|
||||
|
||||
void
|
||||
crypto_shared_secret(uint8_t **shared_secret_bytes, size_t *shared_secret_bytes_len,
|
||||
@ -72,4 +93,7 @@ crypto_aes_decrypt(uint8_t *encrypted, size_t encrypted_len, struct crypto_aes_c
|
||||
int
|
||||
crypto_base62_to_bin(uint8_t *out, size_t out_len, const char *in);
|
||||
|
||||
int
|
||||
crypto_hashcash_solve(struct crypto_hashcash_solution *solution, struct crypto_hashcash_challenge *challenge, const char **errmsg);
|
||||
|
||||
#endif /* __CRYPTO_H__ */
|
||||
|
@ -81,8 +81,11 @@
|
||||
#define SP_CLIENT_VERSION_DEFAULT "0.0.0"
|
||||
#define SP_CLIENT_BUILD_ID_DEFAULT "aabbccdd"
|
||||
|
||||
// ClientIdHex from client_id.go
|
||||
#define SP_CLIENT_ID_HEX "65b708073fc0480ea92a077233ca87bd"
|
||||
// ClientIdHex from client_id.go. This seems to be the id that Spotify's own app
|
||||
// uses. It is used in the call to https://clienttoken.spotify.com/v1/clienttoken.
|
||||
// The endpoint doesn't accept client ID's of app registered at
|
||||
// develop.spotify.com, so unfortunately spoofing is required.
|
||||
#define SP_CLIENT_ID_DEFAULT "65b708073fc0480ea92a077233ca87bd"
|
||||
|
||||
// Shorthand for error handling
|
||||
#define RETURN_ERROR(r, m) \
|
||||
@ -382,6 +385,9 @@ struct sp_session
|
||||
struct sp_token http_clienttoken;
|
||||
struct sp_token http_accesstoken;
|
||||
|
||||
int n_hashcash_challenges;
|
||||
struct crypto_hashcash_challenge *hashcash_challenges;
|
||||
|
||||
bool is_logged_in;
|
||||
struct sp_credentials credentials;
|
||||
char country[3]; // Incl null term
|
||||
|
@ -1016,6 +1016,8 @@ system_info_set(struct sp_sysinfo *si_out, struct sp_sysinfo *si_user)
|
||||
|
||||
if (si_out->client_name[0] == '\0')
|
||||
snprintf(si_out->client_name, sizeof(si_out->client_name), SP_CLIENT_NAME_DEFAULT);
|
||||
if (si_out->client_id[0] == '\0')
|
||||
snprintf(si_out->client_id, sizeof(si_out->client_id), SP_CLIENT_ID_DEFAULT);
|
||||
if (si_out->client_version[0] == '\0')
|
||||
snprintf(si_out->client_version, sizeof(si_out->client_version), SP_CLIENT_VERSION_DEFAULT);
|
||||
if (si_out->client_build_id[0] == '\0')
|
||||
@ -1035,7 +1037,6 @@ librespotc_init(struct sp_sysinfo *sysinfo, struct sp_callbacks *callbacks)
|
||||
RETURN_ERROR(SP_ERR_INVALID, "Bug! Misalignment between enum seq_type and seq_requests");
|
||||
|
||||
sp_cb = *callbacks;
|
||||
sp_initialized = true;
|
||||
|
||||
system_info_set(&sp_sysinfo, sysinfo);
|
||||
|
||||
@ -1054,6 +1055,7 @@ librespotc_init(struct sp_sysinfo *sysinfo, struct sp_callbacks *callbacks)
|
||||
if (sp_cb.thread_name_set)
|
||||
sp_cb.thread_name_set(sp_tid);
|
||||
|
||||
sp_initialized = true;
|
||||
return 0;
|
||||
|
||||
error:
|
||||
|
Loading…
x
Reference in New Issue
Block a user