Add lib
This commit is contained in:
21
lib/ShazamIO/LICENSE.txt
Normal file
21
lib/ShazamIO/LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 dotX12
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
38
lib/ShazamIO/pyproject.toml
Normal file
38
lib/ShazamIO/pyproject.toml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "shazamio"
|
||||||
|
version = "0.0.6"
|
||||||
|
description = "Is a asynchronous framework from reverse engineered Shazam API written in Python 3.8+ with asyncio and aiohttp."
|
||||||
|
authors = ["dotX12"]
|
||||||
|
license = "MIT License"
|
||||||
|
keywords = ["python", "shazam", "music", "recognize", "api", "async", "asyncio", "aiohttp", "identification"]
|
||||||
|
readme = "README.md"
|
||||||
|
homepage = "https://github.com/dotX12/ShazamIO"
|
||||||
|
repository = "https://github.com/dotX12/ShazamIO"
|
||||||
|
include = [
|
||||||
|
"README.md",
|
||||||
|
"LICENSE.txt"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.8"
|
||||||
|
numpy = "^1.24.0"
|
||||||
|
aiohttp = "^3.8.3"
|
||||||
|
pydub = "^0.25.1"
|
||||||
|
dataclass-factory = "2.16"
|
||||||
|
aiofiles = "*"
|
||||||
|
anyio = "^3.6.2"
|
||||||
|
pydantic = "*"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0", "wheel>=0.36,<1.0", "poetry>=1.1,<2", "virtualenv==20.0.33"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "-scoped"
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
filterwarnings = ["ignore::DeprecationWarning"]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 100
|
||||||
6
lib/ShazamIO/shazamio/__init__.py
Normal file
6
lib/ShazamIO/shazamio/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from .serializers import Serialize
|
||||||
|
from .api import Shazam
|
||||||
|
from .converter import Geo
|
||||||
|
from .enums import GenreMusic
|
||||||
|
|
||||||
|
__all__ = ("Serialize", "Shazam", "Geo", "GenreMusic")
|
||||||
280
lib/ShazamIO/shazamio/algorithm.py
Normal file
280
lib/ShazamIO/shazamio/algorithm.py
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
from copy import copy
|
||||||
|
from typing import List, Optional, Any
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .enums import FrequencyBand
|
||||||
|
from .signature import DecodedMessage, FrequencyPeak
|
||||||
|
|
||||||
|
HANNING_MATRIX = np.hanning(2050)[1:-1] # Wipe trailing and leading zeroes
|
||||||
|
|
||||||
|
|
||||||
|
class RingBuffer(list):
|
||||||
|
def __init__(self, buffer_size: int, default_value: Any = None):
|
||||||
|
if default_value is not None:
|
||||||
|
list.__init__(self, [copy(default_value) for _ in range(buffer_size)])
|
||||||
|
else:
|
||||||
|
list.__init__(self, [None] * buffer_size)
|
||||||
|
|
||||||
|
self.position: int = 0
|
||||||
|
self.buffer_size: int = buffer_size
|
||||||
|
self.num_written: int = 0
|
||||||
|
|
||||||
|
def append(self, value: Any):
|
||||||
|
self[self.position] = value
|
||||||
|
|
||||||
|
self.position += 1
|
||||||
|
self.position %= self.buffer_size
|
||||||
|
self.num_written += 1
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureGenerator:
|
||||||
|
def __init__(self):
|
||||||
|
# Used when storing input that will be processed when requiring to
|
||||||
|
# generate a signature:
|
||||||
|
|
||||||
|
self.input_pending_processing: List[int] = []
|
||||||
|
# Signed 16-bits, 16 KHz mono samples to be processed
|
||||||
|
|
||||||
|
self.samples_processed: int = 0
|
||||||
|
# Number of samples processed out of "self.input_pending_processing"
|
||||||
|
|
||||||
|
# Used when processing input:
|
||||||
|
|
||||||
|
self.ring_buffer_of_samples: RingBuffer[int] = RingBuffer(buffer_size=2048, default_value=0)
|
||||||
|
|
||||||
|
self.fft_outputs: RingBuffer[List[float]] = RingBuffer(
|
||||||
|
buffer_size=256, default_value=[0.0 * 1025]
|
||||||
|
)
|
||||||
|
# Lists of 1025 floats, premultiplied with a Hanning function before being
|
||||||
|
# passed through FFT, computed from
|
||||||
|
# the ring buffer every new 128 samples
|
||||||
|
|
||||||
|
self.spread_fft_output: RingBuffer[List[float]] = RingBuffer(
|
||||||
|
buffer_size=256, default_value=[0] * 1025
|
||||||
|
)
|
||||||
|
|
||||||
|
# How much data to send to Shazam at once?
|
||||||
|
|
||||||
|
self.MAX_TIME_SECONDS = 3.1
|
||||||
|
self.MAX_PEAKS = 255
|
||||||
|
|
||||||
|
# The object that will hold information about the next fingerpring
|
||||||
|
# to be produced
|
||||||
|
|
||||||
|
self.next_signature = DecodedMessage()
|
||||||
|
self.next_signature.sample_rate_hz = 16000
|
||||||
|
self.next_signature.number_samples = 0
|
||||||
|
self.next_signature.frequency_band_to_sound_peaks = {}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Add data to be generated a signature for, which will be
|
||||||
|
processed when self.get_next_signature() is called. This
|
||||||
|
function expects signed 16-bit 16 KHz mono PCM samples.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def feed_input(self, s16le_mono_samples: List[int]):
|
||||||
|
self.input_pending_processing += s16le_mono_samples
|
||||||
|
|
||||||
|
"""
|
||||||
|
Consume some of the samples fed to self.feed_input(), and return
|
||||||
|
a Shazam signature (DecodedMessage object) to be sent to servers
|
||||||
|
once "enough data has been gathered".
|
||||||
|
|
||||||
|
Except if there are no more samples to be consumed, in this case
|
||||||
|
we will return None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_next_signature(self) -> Optional[DecodedMessage]:
|
||||||
|
if len(self.input_pending_processing) - self.samples_processed < 128:
|
||||||
|
return None
|
||||||
|
while len(self.input_pending_processing) - self.samples_processed >= 128 and (
|
||||||
|
self.next_signature.number_samples / self.next_signature.sample_rate_hz
|
||||||
|
< self.MAX_TIME_SECONDS
|
||||||
|
or sum(
|
||||||
|
len(peaks) for peaks in self.next_signature.frequency_band_to_sound_peaks.values()
|
||||||
|
)
|
||||||
|
< self.MAX_PEAKS
|
||||||
|
):
|
||||||
|
self.process_input(
|
||||||
|
self.input_pending_processing[self.samples_processed : self.samples_processed + 128]
|
||||||
|
)
|
||||||
|
self.samples_processed += 128
|
||||||
|
|
||||||
|
returned_signature = self.next_signature
|
||||||
|
|
||||||
|
self.next_signature = DecodedMessage()
|
||||||
|
self.next_signature.sample_rate_hz = 16000
|
||||||
|
self.next_signature.number_samples = 0
|
||||||
|
self.next_signature.frequency_band_to_sound_peaks = {}
|
||||||
|
|
||||||
|
self.ring_buffer_of_samples: RingBuffer[int] = RingBuffer(buffer_size=2048, default_value=0)
|
||||||
|
self.fft_outputs: RingBuffer[List[float]] = RingBuffer(
|
||||||
|
buffer_size=256, default_value=[0.0 * 1025]
|
||||||
|
)
|
||||||
|
self.spread_fft_output: RingBuffer[List[float]] = RingBuffer(
|
||||||
|
buffer_size=256, default_value=[0] * 1025
|
||||||
|
)
|
||||||
|
|
||||||
|
return returned_signature
|
||||||
|
|
||||||
|
def process_input(self, s16le_mono_samples: List[int]):
|
||||||
|
self.next_signature.number_samples += len(s16le_mono_samples)
|
||||||
|
for position_of_chunk in range(0, len(s16le_mono_samples), 128):
|
||||||
|
self.do_fft(s16le_mono_samples[position_of_chunk : position_of_chunk + 128])
|
||||||
|
self.do_peak_spreading_and_recognition()
|
||||||
|
|
||||||
|
def do_fft(self, batch_of_128_s16le_mono_samples):
|
||||||
|
type_ring = self.ring_buffer_of_samples.position + len(batch_of_128_s16le_mono_samples)
|
||||||
|
self.ring_buffer_of_samples[
|
||||||
|
self.ring_buffer_of_samples.position : type_ring
|
||||||
|
] = batch_of_128_s16le_mono_samples
|
||||||
|
self.ring_buffer_of_samples.position += len(batch_of_128_s16le_mono_samples)
|
||||||
|
self.ring_buffer_of_samples.position %= 2048
|
||||||
|
self.ring_buffer_of_samples.num_written += len(batch_of_128_s16le_mono_samples)
|
||||||
|
|
||||||
|
excerpt_from_ring_buffer: list = (
|
||||||
|
self.ring_buffer_of_samples[self.ring_buffer_of_samples.position :]
|
||||||
|
+ self.ring_buffer_of_samples[: self.ring_buffer_of_samples.position]
|
||||||
|
)
|
||||||
|
|
||||||
|
# The pre multiplication of the array is for applying a windowing function before the DFT
|
||||||
|
# (slight rounded Hanning without zeros at edges)
|
||||||
|
|
||||||
|
fft_results: np.array = np.fft.rfft(HANNING_MATRIX * excerpt_from_ring_buffer)
|
||||||
|
|
||||||
|
fft_results = (fft_results.real**2 + fft_results.imag**2) / (1 << 17)
|
||||||
|
fft_results = np.maximum(fft_results, 0.0000000001)
|
||||||
|
|
||||||
|
self.fft_outputs.append(fft_results)
|
||||||
|
|
||||||
|
def do_peak_spreading_and_recognition(self):
|
||||||
|
self.do_peak_spreading()
|
||||||
|
if self.spread_fft_output.num_written >= 46:
|
||||||
|
self.do_peak_recognition()
|
||||||
|
|
||||||
|
def do_peak_spreading(self):
|
||||||
|
origin_last_fft: List[float] = self.fft_outputs[self.fft_outputs.position - 1]
|
||||||
|
|
||||||
|
temporary_array_1 = np.tile(origin_last_fft, 3).reshape((3, -1))
|
||||||
|
temporary_array_1[1] = np.roll(temporary_array_1[1], -1)
|
||||||
|
temporary_array_1[2] = np.roll(temporary_array_1[2], -2)
|
||||||
|
|
||||||
|
origin_last_fft_np = np.hstack([temporary_array_1.max(axis=0)[:-3], origin_last_fft[-3:]])
|
||||||
|
|
||||||
|
i1, i2, i3 = [
|
||||||
|
(self.spread_fft_output.position + former_fft_num) % self.spread_fft_output.buffer_size
|
||||||
|
for former_fft_num in [-1, -3, -6]
|
||||||
|
]
|
||||||
|
|
||||||
|
temporary_array_2 = np.vstack(
|
||||||
|
[
|
||||||
|
origin_last_fft_np,
|
||||||
|
self.spread_fft_output[i1],
|
||||||
|
self.spread_fft_output[i2],
|
||||||
|
self.spread_fft_output[i3],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
temporary_array_2[1] = np.max(temporary_array_2[:2, :], axis=0)
|
||||||
|
temporary_array_2[2] = np.max(temporary_array_2[:3, :], axis=0)
|
||||||
|
temporary_array_2[3] = np.max(temporary_array_2[:4, :], axis=0)
|
||||||
|
|
||||||
|
self.spread_fft_output[i1] = temporary_array_2[1].tolist()
|
||||||
|
self.spread_fft_output[i2] = temporary_array_2[2].tolist()
|
||||||
|
self.spread_fft_output[i3] = temporary_array_2[3].tolist()
|
||||||
|
|
||||||
|
self.spread_fft_output.append(list(origin_last_fft_np))
|
||||||
|
|
||||||
|
def do_peak_recognition(self):
|
||||||
|
fft_minus_46 = self.fft_outputs[
|
||||||
|
(self.fft_outputs.position - 46) % self.fft_outputs.buffer_size
|
||||||
|
]
|
||||||
|
fft_minus_49 = self.spread_fft_output[
|
||||||
|
(self.spread_fft_output.position - 49) % self.spread_fft_output.buffer_size
|
||||||
|
]
|
||||||
|
|
||||||
|
for bin_position in range(10, 1015):
|
||||||
|
# Ensure that the bin is large enough to be a peak
|
||||||
|
|
||||||
|
if fft_minus_46[bin_position] >= 1 / 64 and (
|
||||||
|
fft_minus_46[bin_position] >= fft_minus_49[bin_position - 1]
|
||||||
|
):
|
||||||
|
# Ensure that it is frequency-domain local minimum
|
||||||
|
|
||||||
|
max_neighbor_in_fft_minus_49 = 0
|
||||||
|
|
||||||
|
for neighbor_offset in [*range(-10, -3, 3), -3, 1, *range(2, 9, 3)]:
|
||||||
|
max_neighbor_in_fft_minus_49 = max(
|
||||||
|
fft_minus_49[bin_position + neighbor_offset],
|
||||||
|
max_neighbor_in_fft_minus_49,
|
||||||
|
)
|
||||||
|
|
||||||
|
if fft_minus_46[bin_position] > max_neighbor_in_fft_minus_49:
|
||||||
|
# Ensure that it is a time-domain local minimum
|
||||||
|
|
||||||
|
max_neighbor_in_other_adjacent_ffts = max_neighbor_in_fft_minus_49
|
||||||
|
|
||||||
|
for other_offset in [
|
||||||
|
-53,
|
||||||
|
-45,
|
||||||
|
*range(165, 201, 7),
|
||||||
|
*range(214, 250, 7),
|
||||||
|
]:
|
||||||
|
max_neighbor_in_other_adjacent_ffts = max(
|
||||||
|
self.spread_fft_output[
|
||||||
|
(self.spread_fft_output.position + other_offset)
|
||||||
|
% self.spread_fft_output.buffer_size
|
||||||
|
][bin_position - 1],
|
||||||
|
max_neighbor_in_other_adjacent_ffts,
|
||||||
|
)
|
||||||
|
|
||||||
|
if fft_minus_46[bin_position] > max_neighbor_in_other_adjacent_ffts:
|
||||||
|
# This is a peak, store the peak
|
||||||
|
|
||||||
|
fft_number = self.spread_fft_output.num_written - 46
|
||||||
|
|
||||||
|
peak_magnitude = (
|
||||||
|
np.log(max(1 / 64, fft_minus_46[bin_position])) * 1477.3 + 6144
|
||||||
|
)
|
||||||
|
peak_magnitude_before = (
|
||||||
|
np.log(max(1 / 64, fft_minus_46[bin_position - 1])) * 1477.3 + 6144
|
||||||
|
)
|
||||||
|
peak_magnitude_after = (
|
||||||
|
np.log(max(1 / 64, fft_minus_46[bin_position + 1])) * 1477.3 + 6144
|
||||||
|
)
|
||||||
|
|
||||||
|
peak_variation_1 = (
|
||||||
|
peak_magnitude * 2 - peak_magnitude_before - peak_magnitude_after
|
||||||
|
)
|
||||||
|
peak_variation_2 = (
|
||||||
|
(peak_magnitude_after - peak_magnitude_before) * 32 / peak_variation_1
|
||||||
|
)
|
||||||
|
|
||||||
|
corrected_peak_frequency_bin = bin_position * 64 + peak_variation_2
|
||||||
|
|
||||||
|
assert peak_variation_1 > 0
|
||||||
|
|
||||||
|
frequency_hz = corrected_peak_frequency_bin * (16000 / 2 / 1024 / 64)
|
||||||
|
|
||||||
|
if 250 < frequency_hz < 520:
|
||||||
|
band = FrequencyBand.hz_250_520
|
||||||
|
elif 520 < frequency_hz < 1450:
|
||||||
|
band = FrequencyBand.hz_520_1450
|
||||||
|
elif 1450 < frequency_hz < 3500:
|
||||||
|
band = FrequencyBand.hz_1450_3500
|
||||||
|
elif 5500 < frequency_hz <= 5500:
|
||||||
|
band = FrequencyBand.hz_3500_5500
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if band not in self.next_signature.frequency_band_to_sound_peaks:
|
||||||
|
self.next_signature.frequency_band_to_sound_peaks[band] = []
|
||||||
|
|
||||||
|
self.next_signature.frequency_band_to_sound_peaks[band].append(
|
||||||
|
FrequencyPeak(
|
||||||
|
fft_number,
|
||||||
|
int(peak_magnitude),
|
||||||
|
int(corrected_peak_frequency_bin),
|
||||||
|
16000,
|
||||||
|
)
|
||||||
|
)
|
||||||
378
lib/ShazamIO/shazamio/api.py
Normal file
378
lib/ShazamIO/shazamio/api.py
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
import pathlib
|
||||||
|
import uuid
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydub import AudioSegment
|
||||||
|
|
||||||
|
from typing import Dict, Any, Union
|
||||||
|
|
||||||
|
from .misc import Request
|
||||||
|
from .misc import ShazamUrl
|
||||||
|
from .schemas.artists import ArtistQuery
|
||||||
|
from .signature import DecodedMessage
|
||||||
|
from .enums import GenreMusic
|
||||||
|
from .converter import Converter, Geo
|
||||||
|
from .typehints import CountryCode
|
||||||
|
from .utils import ArtistQueryGenerator
|
||||||
|
from .utils import get_song
|
||||||
|
|
||||||
|
|
||||||
|
class Shazam(Converter, Geo, Request):
|
||||||
|
"""Is asynchronous framework for reverse engineered Shazam API written in Python 3.7 with
|
||||||
|
asyncio and aiohttp."""
|
||||||
|
|
||||||
|
def __init__(self, language: str = "en-US", endpoint_country: str = "GB"):
|
||||||
|
super().__init__(language=language)
|
||||||
|
self.language = language
|
||||||
|
self.endpoint_country = endpoint_country
|
||||||
|
|
||||||
|
async def top_world_tracks(self, limit: int = 200, offset: int = 0) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Search top world tracks
|
||||||
|
|
||||||
|
:param limit: Determines how many songs the maximum can be in the request.
|
||||||
|
Example: If 5 is specified, the query will return no more than 5 songs.
|
||||||
|
:param offset: A parameter that determines with which song to display the request.
|
||||||
|
The default is 0. If you want to skip the first few songs, set this parameter to
|
||||||
|
your own.
|
||||||
|
:return: dict tracks
|
||||||
|
"""
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
ShazamUrl.TOP_TRACKS_WORLD.format(
|
||||||
|
language=self.language,
|
||||||
|
endpoint_country=self.endpoint_country,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
),
|
||||||
|
headers=self.headers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def artist_about(
|
||||||
|
self, artist_id: int, query: Optional[ArtistQuery] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieving information from an artist profile
|
||||||
|
|
||||||
|
:param artist_id: Artist number. Example (203347991)
|
||||||
|
:param query: Foo
|
||||||
|
https://www.shazam.com/artist/203347991/
|
||||||
|
:return: dict about artist
|
||||||
|
"""
|
||||||
|
|
||||||
|
if query:
|
||||||
|
pg = ArtistQueryGenerator(source=query)
|
||||||
|
params_dict = pg.params()
|
||||||
|
else:
|
||||||
|
params_dict = {}
|
||||||
|
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
ShazamUrl.SEARCH_ARTIST_V2.format(
|
||||||
|
endpoint_country=self.endpoint_country,
|
||||||
|
artist_id=artist_id,
|
||||||
|
),
|
||||||
|
params=params_dict,
|
||||||
|
headers=self.headers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def track_about(self, track_id: int) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get track information
|
||||||
|
|
||||||
|
:param track_id: Track number. Example: (549952578)
|
||||||
|
https://www.shazam.com/track/549952578/
|
||||||
|
:return: dict about track
|
||||||
|
"""
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
ShazamUrl.ABOUT_TRACK.format(
|
||||||
|
language=self.language,
|
||||||
|
endpoint_country=self.endpoint_country,
|
||||||
|
track_id=track_id,
|
||||||
|
),
|
||||||
|
headers=self.headers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def top_country_tracks(
|
||||||
|
self,
|
||||||
|
country_code: Union[CountryCode, str],
|
||||||
|
limit: int = 200,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get the best tracks by country code
|
||||||
|
https://www.shazam.com/charts/discovery/netherlands
|
||||||
|
|
||||||
|
:param country_code: ISO 3166-3 alpha-2 code. Example: RU,NL,UA
|
||||||
|
:param limit: Determines how many songs the maximum can be in the request.
|
||||||
|
Example: If 5 is specified, the query will return no more than 5 songs.
|
||||||
|
:param offset: A parameter that determines with which song to display the request.
|
||||||
|
The default is 0. If you want to skip the first few songs, set this parameter to
|
||||||
|
your own.
|
||||||
|
:return: dict songs
|
||||||
|
"""
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
ShazamUrl.TOP_TRACKS_COUNTRY.format(
|
||||||
|
language=self.language,
|
||||||
|
endpoint_country=self.endpoint_country,
|
||||||
|
country_code=country_code,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
),
|
||||||
|
headers=self.headers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def top_city_tracks(
|
||||||
|
self,
|
||||||
|
country_code: Union[CountryCode, str],
|
||||||
|
city_name: str,
|
||||||
|
limit: int = 200,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieving information from an artist profile
|
||||||
|
https://www.shazam.com/charts/top-50/russia/moscow
|
||||||
|
|
||||||
|
:param country_code: ISO 3166-3 alpha-2 code. Example: RU,NL,UA
|
||||||
|
:param city_name: City name from https://github.com/dotX12/dotX12/blob/main/city.json
|
||||||
|
Example: Budapest, Moscow
|
||||||
|
:param limit: Determines how many songs the maximum can be in the request.
|
||||||
|
Example: If 5 is specified, the query will return no more than 5 songs.
|
||||||
|
:param offset: A parameter that determines with which song to display the request.
|
||||||
|
The default is 0. If you want to skip the first few songs, set this parameter to
|
||||||
|
your own.
|
||||||
|
:return: dict songs
|
||||||
|
"""
|
||||||
|
city_id = await self.city_id_from(country=country_code, city=city_name)
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
ShazamUrl.TOP_TRACKS_CITY.format(
|
||||||
|
language=self.language,
|
||||||
|
endpoint_country=self.endpoint_country,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
city_id=city_id,
|
||||||
|
),
|
||||||
|
headers=self.headers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def top_world_genre_tracks(
|
||||||
|
self,
|
||||||
|
genre: Union[GenreMusic, int],
|
||||||
|
limit: int = 100,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get world tracks by certain genre
|
||||||
|
https://www.shazam.com/charts/genre/world/rock
|
||||||
|
|
||||||
|
:param genre: Genre name or ID:
|
||||||
|
POP = 1, HIP_HOP_RAP = 2, DANCE = 3, ELECTRONIC = 4, RNB_SOUL = 5, ALTERNATIVE =
|
||||||
|
6, ROCK = 7
|
||||||
|
LATIN = 8, FILM_TV_STAGE = 9, COUNTRY = 10, AFRO_BEATS = 11, WORLDWIDE = 12,
|
||||||
|
REGGAE_DANCE_HALL = 13
|
||||||
|
HOUSE = 14, K_POP = 15, FRENCH_POP = 16, SINGER_SONGWRITER = 17,
|
||||||
|
REGIONAL_MEXICANO = 18
|
||||||
|
|
||||||
|
:param limit: Determines how many songs the maximum can be in the request.
|
||||||
|
Example: If 5 is specified, the query will return no more than 5 songs.
|
||||||
|
:param offset: A parameter that determines with which song to display the request.
|
||||||
|
The default is 0. If you want to skip the first few songs, set this parameter
|
||||||
|
to your own.
|
||||||
|
:return: dict songs
|
||||||
|
"""
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
ShazamUrl.GENRE_WORLD.format(
|
||||||
|
language=self.language,
|
||||||
|
endpoint_country=self.endpoint_country,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
genre=genre,
|
||||||
|
),
|
||||||
|
headers=self.headers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def top_country_genre_tracks(
|
||||||
|
self,
|
||||||
|
country_code: str,
|
||||||
|
genre: Union[GenreMusic, int],
|
||||||
|
limit: int = 200,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
The best tracks by a genre in the country
|
||||||
|
https://www.shazam.com/charts/genre/spain/hip-hop-rap
|
||||||
|
:param country_code: ISO 3166-3 alpha-2 code. Example: RU,NL,UA
|
||||||
|
:param genre: Genre name or ID:
|
||||||
|
POP = 1, HIP_HOP_RAP = 2, DANCE = 3, ELECTRONIC = 4, RNB_SOUL = 5, ALTERNATIVE =
|
||||||
|
6, ROCK = 7
|
||||||
|
LATIN = 8, FILM_TV_STAGE = 9, COUNTRY = 10, AFRO_BEATS = 11, WORLDWIDE = 12,
|
||||||
|
REGGAE_DANCE_HALL = 13
|
||||||
|
HOUSE = 14, K_POP = 15, FRENCH_POP = 16, SINGER_SONGWRITER = 17,
|
||||||
|
REGIONAL_MEXICANO = 18
|
||||||
|
:param limit: Determines how many songs the maximum can be in the request.
|
||||||
|
Example: If 5 is specified, the query will return no more than 5 songs
|
||||||
|
:param offset: A parameter that determines with which song to display the request.
|
||||||
|
The default is 0. If you want to skip the first few songs, set this parameter to
|
||||||
|
your own.
|
||||||
|
:return: dict songs
|
||||||
|
"""
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
ShazamUrl.GENRE_COUNTRY.format(
|
||||||
|
language=self.language,
|
||||||
|
endpoint_country=self.endpoint_country,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
country=country_code,
|
||||||
|
genre=genre,
|
||||||
|
),
|
||||||
|
headers=self.headers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def related_tracks(
|
||||||
|
self,
|
||||||
|
track_id: int,
|
||||||
|
limit: int = 20,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Similar songs based song id
|
||||||
|
https://www.shazam.com/track/546891609/2-phu%CC%81t-ho%CC%9Bn-kaiz-remix
|
||||||
|
:param track_id: Track number. Example: (549952578)
|
||||||
|
https://www.shazam.com/track/549952578/
|
||||||
|
:param limit: Determines how many songs the maximum can be in the request.
|
||||||
|
Example: If 5 is specified, the query will return no more than 5 songs
|
||||||
|
:param offset: A parameter that determines with which song to display the request.
|
||||||
|
The default is 0. If you want to skip the first few songs, set this parameter to
|
||||||
|
your own.
|
||||||
|
:return: dict tracks
|
||||||
|
"""
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
ShazamUrl.RELATED_SONGS.format(
|
||||||
|
language=self.language,
|
||||||
|
endpoint_country=self.endpoint_country,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
track_id=track_id,
|
||||||
|
),
|
||||||
|
headers=self.headers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def search_artist(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
limit: int = 10,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Search all artists by prefix or fullname
|
||||||
|
:param query: Artist name or search prefix
|
||||||
|
:param limit: Determines how many artists the maximum can be in the request.
|
||||||
|
Example: If 5 is specified, the query will return no more than 5 artists.
|
||||||
|
:param offset: A parameter that determines with which song to display the request.
|
||||||
|
The default is 0. If you want to skip the first few songs, set this parameter to
|
||||||
|
your own.
|
||||||
|
:return: dict artists
|
||||||
|
"""
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
ShazamUrl.SEARCH_ARTIST.format(
|
||||||
|
language=self.language,
|
||||||
|
endpoint_country=self.endpoint_country,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
query=query,
|
||||||
|
),
|
||||||
|
headers=self.headers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def search_track(self, query: str, limit: int = 10, offset: int = 0) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Search all tracks by prefix
|
||||||
|
:param query: Track full title or prefix title
|
||||||
|
:param limit: Determines how many songs the maximum can be in the request.
|
||||||
|
Example: If 5 is specified, the query will return no more than 5 songs.
|
||||||
|
:param offset: A parameter that determines with which song to display the request.
|
||||||
|
The default is 0. If you want to skip the first few songs, set this parameter to
|
||||||
|
your own.
|
||||||
|
:return: dict songs
|
||||||
|
"""
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
ShazamUrl.SEARCH_MUSIC.format(
|
||||||
|
language=self.language,
|
||||||
|
endpoint_country=self.endpoint_country,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
query=query,
|
||||||
|
),
|
||||||
|
headers=self.headers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def listening_counter(self, track_id: int) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Returns the total track listener counter.
|
||||||
|
:param track_id: Track number. Example: (559284007)
|
||||||
|
https://www.shazam.com/track/559284007/rampampam
|
||||||
|
:return: The data dictionary that contains the listen counter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return await self.request(
|
||||||
|
"GET",
|
||||||
|
ShazamUrl.LISTENING_COUNTER.format(
|
||||||
|
track_id,
|
||||||
|
language=self.language,
|
||||||
|
),
|
||||||
|
headers=self.headers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_youtube_data(self, link: str) -> Dict[str, Any]:
|
||||||
|
return await self.request("GET", link, headers=self.headers())
|
||||||
|
|
||||||
|
async def recognize_song(
|
||||||
|
self, data: Union[str, pathlib.Path, bytes, bytearray, AudioSegment]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Creating a song signature based on a file and searching for this signature in the shazam
|
||||||
|
database.
|
||||||
|
:param data: Path to song file or bytes
|
||||||
|
:return: Dictionary with information about the found song
|
||||||
|
"""
|
||||||
|
song = await get_song(data=data)
|
||||||
|
audio = self.normalize_audio_data(song)
|
||||||
|
signature_generator = self.create_signature_generator(audio)
|
||||||
|
signature = signature_generator.get_next_signature()
|
||||||
|
|
||||||
|
if len(signature_generator.input_pending_processing) < 128:
|
||||||
|
return {"matches": []}
|
||||||
|
|
||||||
|
while not signature:
|
||||||
|
signature = signature_generator.get_next_signature()
|
||||||
|
results = await self.send_recognize_request(signature)
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def send_recognize_request(self, sig: DecodedMessage) -> Dict[str, Any]:
|
||||||
|
data = Converter.data_search(
|
||||||
|
Request.TIME_ZONE,
|
||||||
|
sig.encode_to_uri(),
|
||||||
|
int(sig.number_samples / sig.sample_rate_hz * 1000),
|
||||||
|
int(time.time() * 1000),
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self.request(
|
||||||
|
"POST",
|
||||||
|
ShazamUrl.SEARCH_FROM_FILE.format(
|
||||||
|
language=self.language,
|
||||||
|
endpoint_country=self.endpoint_country,
|
||||||
|
uuid_1=str(uuid.uuid4()).upper(),
|
||||||
|
uuid_2=str(uuid.uuid4()).upper(),
|
||||||
|
),
|
||||||
|
headers=self.headers(),
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
18
lib/ShazamIO/shazamio/client.py
Normal file
18
lib/ShazamIO/shazamio/client.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from shazamio.exceptions import BadMethod
|
||||||
|
from shazamio.utils import validate_json
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPClient:
|
||||||
|
@staticmethod
|
||||||
|
async def request(method: str, url: str, *args, **kwargs) -> dict:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
if method.upper() == "GET":
|
||||||
|
async with session.get(url, **kwargs) as resp:
|
||||||
|
return await validate_json(resp, *args)
|
||||||
|
elif method.upper() == "POST":
|
||||||
|
async with session.post(url, **kwargs) as resp:
|
||||||
|
return await validate_json(resp, *args)
|
||||||
|
else:
|
||||||
|
raise BadMethod("Accept only GET/POST")
|
||||||
64
lib/ShazamIO/shazamio/converter.py
Normal file
64
lib/ShazamIO/shazamio/converter.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from pydub import AudioSegment
|
||||||
|
from shazamio.algorithm import SignatureGenerator
|
||||||
|
from shazamio.client import HTTPClient
|
||||||
|
from shazamio.exceptions import BadCityName, BadCountryName
|
||||||
|
from shazamio.misc import ShazamUrl
|
||||||
|
from shazamio.schemas.models import *
|
||||||
|
from shazamio.typehints import CountryCode
|
||||||
|
|
||||||
|
|
||||||
|
class Geo(HTTPClient):
|
||||||
|
async def city_id_from(self, country: Union[CountryCode, str], city: str) -> int:
|
||||||
|
"""
|
||||||
|
Return City ID from country name and city name.
|
||||||
|
:param country: - Country name
|
||||||
|
:param city: - City name
|
||||||
|
:return: City ID
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = await self.request("GET", ShazamUrl.CITY_IDS, "text/plain")
|
||||||
|
for response_country in data["countries"]:
|
||||||
|
if country == response_country["id"]:
|
||||||
|
for response_city in response_country["cities"]:
|
||||||
|
if city == response_city["name"]:
|
||||||
|
return response_city["id"]
|
||||||
|
raise BadCityName("City not found, check city name")
|
||||||
|
|
||||||
|
async def all_cities_from_country(self, country: Union[CountryCode, str]) -> list:
|
||||||
|
cities = []
|
||||||
|
data = await self.request("GET", ShazamUrl.CITY_IDS, "text/plain")
|
||||||
|
for response_country in data["countries"]:
|
||||||
|
if country == response_country["id"]:
|
||||||
|
for city in response_country["cities"]:
|
||||||
|
cities.append(city["name"])
|
||||||
|
return cities
|
||||||
|
raise BadCountryName("Country not found, check country name")
|
||||||
|
|
||||||
|
|
||||||
|
class Converter:
|
||||||
|
@staticmethod
|
||||||
|
def data_search(timezone: str, uri: str, samplems: int, timestamp: int) -> dict:
|
||||||
|
return {
|
||||||
|
"timezone": timezone,
|
||||||
|
"signature": {"uri": uri, "samplems": samplems},
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"context": {},
|
||||||
|
"geolocation": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def normalize_audio_data(audio: AudioSegment) -> AudioSegment:
|
||||||
|
audio = audio.set_sample_width(2)
|
||||||
|
audio = audio.set_frame_rate(16000)
|
||||||
|
audio = audio.set_channels(1)
|
||||||
|
|
||||||
|
return audio
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_signature_generator(audio: AudioSegment) -> SignatureGenerator:
|
||||||
|
signature_generator = SignatureGenerator()
|
||||||
|
signature_generator.feed_input(audio.get_array_of_samples())
|
||||||
|
signature_generator.MAX_TIME_SECONDS = 12
|
||||||
|
if audio.duration_seconds > 12 * 3:
|
||||||
|
signature_generator.samples_processed += 16000 * (int(audio.duration_seconds / 2) - 6)
|
||||||
|
return signature_generator
|
||||||
41
lib/ShazamIO/shazamio/enums.py
Normal file
41
lib/ShazamIO/shazamio/enums.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class GenreMusic(IntEnum):
|
||||||
|
POP = 1
|
||||||
|
HIP_HOP_RAP = 2
|
||||||
|
DANCE = 3
|
||||||
|
ELECTRONIC = 4
|
||||||
|
RNB_SOUL = 5
|
||||||
|
ALTERNATIVE = 6
|
||||||
|
ROCK = 7
|
||||||
|
LATIN = 8
|
||||||
|
FILM_TV_STAGE = 9
|
||||||
|
COUNTRY = 10
|
||||||
|
AFRO_BEATS = 11
|
||||||
|
WORLDWIDE = 12
|
||||||
|
REGGAE_DANCE_HALL = 13
|
||||||
|
HOUSE = 14
|
||||||
|
K_POP = 15
|
||||||
|
FRENCH_POP = 16
|
||||||
|
SINGER_SONGWRITER = 17
|
||||||
|
REGIONAL_MEXICANO = 18
|
||||||
|
|
||||||
|
|
||||||
|
class SampleRate(IntEnum):
|
||||||
|
# Enum keys are sample rates in Hz
|
||||||
|
_8000 = 1
|
||||||
|
_11025 = 2
|
||||||
|
_16000 = 3
|
||||||
|
_32000 = 4
|
||||||
|
_44100 = 5
|
||||||
|
_48000 = 6
|
||||||
|
|
||||||
|
|
||||||
|
class FrequencyBand(IntEnum):
|
||||||
|
# Enum keys are frequency ranges in Hz
|
||||||
|
hz_0_250 = -1 # Nothing above 250 Hz is actually stored
|
||||||
|
hz_250_520 = 0
|
||||||
|
hz_520_1450 = 1
|
||||||
|
hz_1450_3500 = 2
|
||||||
|
hz_3500_5500 = 3 # This one (3.5 KHz - 5.5 KHz) should not be used in legacy mode
|
||||||
14
lib/ShazamIO/shazamio/exceptions.py
Normal file
14
lib/ShazamIO/shazamio/exceptions.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
class FailedDecodeJson(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BadCityName(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BadCountryName(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BadMethod(Exception):
|
||||||
|
pass
|
||||||
133
lib/ShazamIO/shazamio/factory.py
Normal file
133
lib/ShazamIO/shazamio/factory.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
from dataclass_factory import Schema
|
||||||
|
|
||||||
|
|
||||||
|
class FactorySchemas:
|
||||||
|
FACTORY_TRACK_SCHEMA = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"photo_url": ("images", "coverarthq"),
|
||||||
|
"ringtone": ("hub", "actions", 1, "uri"),
|
||||||
|
"artist_id": ("artists", 0, "id"),
|
||||||
|
"apple_music_url": ("hub", "options", 0, "actions", 0, "uri"),
|
||||||
|
"spotify_url": ("hub", "providers", 0, "actions", 0, "uri"),
|
||||||
|
"spotify_uri": ("hub", "providers", 0, "actions", 1, "uri"),
|
||||||
|
"sections": "sections",
|
||||||
|
},
|
||||||
|
skip_internal=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_ARTIST_SCHEMA = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"avatar": "avatar",
|
||||||
|
"genres": ("genres", "secondaries"),
|
||||||
|
"genres_primary": ("genres", "primary"),
|
||||||
|
"adam_id": "adamid",
|
||||||
|
"url": "weburl",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_SONG_SECTION_SCHEMA = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"type": "type",
|
||||||
|
"meta_pages": "metapages",
|
||||||
|
"tab_name": "tabname",
|
||||||
|
"metadata": "metadata",
|
||||||
|
},
|
||||||
|
skip_internal=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_VIDEO_SECTION_SCHEMA = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"type": "type",
|
||||||
|
"youtube_url": "youtubeurl",
|
||||||
|
"tab_name": "tabname",
|
||||||
|
},
|
||||||
|
skip_internal=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_RELATED_SECTION_SCHEMA = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"type": "type",
|
||||||
|
"url": "url",
|
||||||
|
"tab_name": "tabname",
|
||||||
|
},
|
||||||
|
skip_internal=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_YOUTUBE_TRACK_SCHEMA = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"caption": "caption",
|
||||||
|
"image": "image",
|
||||||
|
"actions": "actions",
|
||||||
|
},
|
||||||
|
skip_internal=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_RESPONSE_TRACK_SCHEMA = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"matches": "matches",
|
||||||
|
"location": "location",
|
||||||
|
"retry_ms": "retryms",
|
||||||
|
"timestamp": "timestamp",
|
||||||
|
"timezone": "timezone",
|
||||||
|
"track": "track",
|
||||||
|
"tag_id": "tagid",
|
||||||
|
},
|
||||||
|
skip_internal=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_LYRICS_SECTION = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"type": "type",
|
||||||
|
"text": "text",
|
||||||
|
"footer": "footer",
|
||||||
|
"tab_name": "tabname",
|
||||||
|
"beacon_data": "beacondata",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_BEACON_DATA_LYRICS_SECTION = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"lyrics_id": "lyricsid",
|
||||||
|
"provider_name": "providername",
|
||||||
|
"common_track_id": "commontrackid",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_ARTIST_SECTION = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"type": "type",
|
||||||
|
"id": "id",
|
||||||
|
"name": "name",
|
||||||
|
"verified": "verified",
|
||||||
|
"actions": "actions",
|
||||||
|
"tab_name": "tabname",
|
||||||
|
"top_tracks": "toptracks",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_MATCH = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"id": "id",
|
||||||
|
"offset": "offset",
|
||||||
|
"channel": "channel",
|
||||||
|
"time_skew": "timeskew",
|
||||||
|
"frequency_skew": "frequencyskew",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_ATTRIBUTES_ARTIST = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"name": "name",
|
||||||
|
"url": "url",
|
||||||
|
"artist_bio": "artistBio",
|
||||||
|
"genre_names": "genreNames",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_ARTIST_V2 = Schema(
|
||||||
|
name_mapping={
|
||||||
|
"id": "id",
|
||||||
|
"type": "type",
|
||||||
|
"attributes": "attributes",
|
||||||
|
}
|
||||||
|
)
|
||||||
44
lib/ShazamIO/shazamio/factory_misc.py
Normal file
44
lib/ShazamIO/shazamio/factory_misc.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from dataclass_factory import Factory
|
||||||
|
|
||||||
|
from shazamio.factory import FactorySchemas
|
||||||
|
from shazamio.schemas.artists import ArtistInfo
|
||||||
|
from shazamio.schemas.artists import ArtistV3
|
||||||
|
from shazamio.schemas.attributes import ArtistAttribute
|
||||||
|
from shazamio.schemas.models import (
|
||||||
|
SongSection,
|
||||||
|
VideoSection,
|
||||||
|
RelatedSection,
|
||||||
|
LyricsSection,
|
||||||
|
BeaconDataLyricsSection,
|
||||||
|
ArtistSection,
|
||||||
|
MatchModel,
|
||||||
|
)
|
||||||
|
from shazamio.schemas.models import TrackInfo
|
||||||
|
from shazamio.schemas.models import YoutubeData
|
||||||
|
from shazamio.schemas.models import ResponseTrack
|
||||||
|
|
||||||
|
|
||||||
|
FACTORY_TRACK = Factory(
|
||||||
|
schemas={
|
||||||
|
TrackInfo: FactorySchemas.FACTORY_TRACK_SCHEMA,
|
||||||
|
SongSection: FactorySchemas.FACTORY_SONG_SECTION_SCHEMA,
|
||||||
|
VideoSection: FactorySchemas.FACTORY_VIDEO_SECTION_SCHEMA,
|
||||||
|
LyricsSection: FactorySchemas.FACTORY_LYRICS_SECTION,
|
||||||
|
BeaconDataLyricsSection: FactorySchemas.FACTORY_BEACON_DATA_LYRICS_SECTION,
|
||||||
|
ArtistSection: FactorySchemas.FACTORY_ARTIST_SECTION,
|
||||||
|
MatchModel: FactorySchemas.FACTORY_MATCH,
|
||||||
|
RelatedSection: FactorySchemas.FACTORY_RELATED_SECTION_SCHEMA,
|
||||||
|
YoutubeData: FactorySchemas.FACTORY_YOUTUBE_TRACK_SCHEMA,
|
||||||
|
ResponseTrack: FactorySchemas.FACTORY_RESPONSE_TRACK_SCHEMA,
|
||||||
|
},
|
||||||
|
debug_path=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
FACTORY_ARTIST = Factory(
|
||||||
|
schemas={
|
||||||
|
ArtistAttribute: FactorySchemas.FACTORY_ATTRIBUTES_ARTIST,
|
||||||
|
ArtistV3: FactorySchemas.FACTORY_ARTIST_V2,
|
||||||
|
ArtistInfo: FactorySchemas.FACTORY_ARTIST_SCHEMA,
|
||||||
|
},
|
||||||
|
debug_path=True,
|
||||||
|
)
|
||||||
69
lib/ShazamIO/shazamio/misc.py
Normal file
69
lib/ShazamIO/shazamio/misc.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
from random import choice
|
||||||
|
from shazamio.user_agent import USER_AGENTS
|
||||||
|
|
||||||
|
|
||||||
|
class ShazamUrl:
|
||||||
|
SEARCH_FROM_FILE = (
|
||||||
|
"https://amp.shazam.com/discovery/v5/{language}/{endpoint_country}/iphone/-/tag"
|
||||||
|
"/{uuid_1}/{uuid_2}?sync=true&webv3=true&sampling=true"
|
||||||
|
"&connected=&shazamapiversion=v3&sharehub=true&hubv5minorversion=v5.1&hidelb=true&video=v3"
|
||||||
|
)
|
||||||
|
TOP_TRACKS_WORLD = (
|
||||||
|
"https://www.shazam.com/shazam/v3/{language}/{endpoint_country}/web/-/tracks"
|
||||||
|
"/ip-global-chart?pageSize={limit}&startFrom={offset}"
|
||||||
|
)
|
||||||
|
ABOUT_TRACK = (
|
||||||
|
"https://www.shazam.com/discovery/v5/{language}/{endpoint_country}/web/-/track"
|
||||||
|
"/{track_id}?shazamapiversion=v3&video=v3 "
|
||||||
|
)
|
||||||
|
TOP_TRACKS_COUNTRY = (
|
||||||
|
"https://www.shazam.com/shazam/v3/{language}/{endpoint_country}/web/-/tracks"
|
||||||
|
"/ip-country-chart-{country_code}?pageSize={limit}&startFrom={offset}"
|
||||||
|
)
|
||||||
|
TOP_TRACKS_CITY = (
|
||||||
|
"https://www.shazam.com/shazam/v3/{language}/{endpoint_country}/web/-/tracks"
|
||||||
|
"/ip-city-chart-{city_id}?pageSize={limit}&startFrom={offset}"
|
||||||
|
)
|
||||||
|
CITY_IDS = "https://raw.githubusercontent.com/dotX12/dotX12/main/city.json"
|
||||||
|
GENRE_WORLD = (
|
||||||
|
"https://www.shazam.com/shazam/v3/{language}/{endpoint_country}/web/-/tracks"
|
||||||
|
"/genre-global-chart-{genre}?pageSize={limit}&startFrom={offset}"
|
||||||
|
)
|
||||||
|
GENRE_COUNTRY = (
|
||||||
|
"https://www.shazam.com/shazam/v3/{language}/{endpoint_country}/web/-/tracks"
|
||||||
|
"/genre-country-chart-{country}-{genre}?pageSize={limit}&startFrom={offset}"
|
||||||
|
)
|
||||||
|
RELATED_SONGS = (
|
||||||
|
"https://cdn.shazam.com/shazam/v3/{language}/{endpoint_country}/web/-/tracks"
|
||||||
|
"/track-similarities-id-{track_id}?startFrom={offset}&pageSize={limit}&connected=&channel="
|
||||||
|
)
|
||||||
|
SEARCH_ARTIST = (
|
||||||
|
"https://www.shazam.com/services/search/v4/{language}/{endpoint_country}/web"
|
||||||
|
"/search?term={query}&limit={limit}&offset={offset}&types=artists"
|
||||||
|
)
|
||||||
|
SEARCH_MUSIC = (
|
||||||
|
"https://www.shazam.com/services/search/v3/{language}/{endpoint_country}/web"
|
||||||
|
"/search?query={query}&numResults={limit}&offset={offset}&types=songs"
|
||||||
|
)
|
||||||
|
LISTENING_COUNTER = "https://www.shazam.com/services/count/v2/web/track/{}"
|
||||||
|
|
||||||
|
SEARCH_ARTIST_V2 = (
|
||||||
|
"https://www.shazam.com/services/amapi/v1/catalog/{endpoint_country}/artists/{artist_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Request:
|
||||||
|
TIME_ZONE = "Europe/Moscow"
|
||||||
|
|
||||||
|
def __init__(self, language: str):
|
||||||
|
self.language = language
|
||||||
|
|
||||||
|
def headers(self):
|
||||||
|
return {
|
||||||
|
"X-Shazam-Platform": "IPHONE",
|
||||||
|
"X-Shazam-AppVersion": "14.1.0",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Language": self.language,
|
||||||
|
"Accept-Encoding": "gzip, deflate",
|
||||||
|
"User-Agent": choice(USER_AGENTS),
|
||||||
|
}
|
||||||
0
lib/ShazamIO/shazamio/schemas/__init__.py
Normal file
0
lib/ShazamIO/shazamio/schemas/__init__.py
Normal file
0
lib/ShazamIO/shazamio/schemas/artist/__init__.py
Normal file
0
lib/ShazamIO/shazamio/schemas/artist/__init__.py
Normal file
53
lib/ShazamIO/shazamio/schemas/artist/views/full_albums.py
Normal file
53
lib/ShazamIO/shazamio/schemas/artist/views/full_albums.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from shazamio.schemas.attributes import AttributeName
|
||||||
|
from shazamio.schemas.base import BaseDataModel
|
||||||
|
from shazamio.schemas.photos import ImageModel
|
||||||
|
|
||||||
|
|
||||||
|
class PlayParams(BaseModel):
|
||||||
|
id: str
|
||||||
|
kind: str
|
||||||
|
|
||||||
|
|
||||||
|
class EditorialArtwork(BaseModel):
|
||||||
|
subscription_hero: Optional[ImageModel] = Field(None, alias="subscriptionHero")
|
||||||
|
store_flow_case: Optional[ImageModel] = Field(None, alias="storeFlowcase")
|
||||||
|
|
||||||
|
|
||||||
|
class EditorialNotes(BaseModel):
|
||||||
|
standard: Optional[str] = None
|
||||||
|
short: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AttributesFullAlbums(BaseModel):
|
||||||
|
copyright: str
|
||||||
|
genre_names: List[str] = Field(..., alias="genreNames")
|
||||||
|
release_date: str = Field(..., alias="releaseDate")
|
||||||
|
is_mastered_for_itunes: bool = Field(..., alias="isMasteredForItunes")
|
||||||
|
upc: str
|
||||||
|
artwork: ImageModel
|
||||||
|
play_params: PlayParams = Field(..., alias="playParams")
|
||||||
|
url: str
|
||||||
|
record_label: str = Field(..., alias="recordLabel")
|
||||||
|
track_count: int = Field(..., alias="trackCount")
|
||||||
|
is_compilation: bool = Field(..., alias="isCompilation")
|
||||||
|
is_prerelease: bool = Field(..., alias="isPrerelease")
|
||||||
|
audio_traits: List[str] = Field(..., alias="audioTraits")
|
||||||
|
editorial_artwork: EditorialArtwork = Field(..., alias="editorialArtwork")
|
||||||
|
is_single: bool = Field(..., alias="isSingle")
|
||||||
|
name: str
|
||||||
|
artist_name: str = Field(..., alias="artistName")
|
||||||
|
content_rating: Optional[str] = Field(None, alias="contentRating")
|
||||||
|
is_complete: bool = Field(..., alias="isComplete")
|
||||||
|
editorial_notes: Optional[EditorialNotes] = Field(None, alias="editorialNotes")
|
||||||
|
|
||||||
|
|
||||||
|
class FullAlbumsModel(BaseModel):
|
||||||
|
href: Optional[str] = None
|
||||||
|
attributes: Optional[AttributeName] = None
|
||||||
|
data: Optional[List[BaseDataModel[AttributesFullAlbums]]] = None
|
||||||
42
lib/ShazamIO/shazamio/schemas/artist/views/last_release.py
Normal file
42
lib/ShazamIO/shazamio/schemas/artist/views/last_release.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from shazamio.schemas.attributes import AttributeName
|
||||||
|
from shazamio.schemas.base import BaseDataModel
|
||||||
|
from shazamio.schemas.photos import ImageModel
|
||||||
|
|
||||||
|
|
||||||
|
class PlayParams(BaseModel):
|
||||||
|
id: str
|
||||||
|
kind: str
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeLastRelease(BaseModel):
|
||||||
|
copyright: str
|
||||||
|
genre_names: List[str] = Field(..., alias="genreNames")
|
||||||
|
release_date: str = Field(..., alias="releaseDate")
|
||||||
|
is_mastered_for_itunes: bool = Field(..., alias="isMasteredForItunes")
|
||||||
|
upc: str
|
||||||
|
artwork: ImageModel
|
||||||
|
play_params: PlayParams = Field(..., alias="playParams")
|
||||||
|
url: str
|
||||||
|
record_label: str = Field(..., alias="recordLabel")
|
||||||
|
track_count: int = Field(..., alias="trackCount")
|
||||||
|
is_compilation: bool = Field(..., alias="isCompilation")
|
||||||
|
is_prerelease: bool = Field(..., alias="isPrerelease")
|
||||||
|
audio_traits: List[str] = Field(..., alias="audioTraits")
|
||||||
|
editorial_artwork: Dict[str, Any] = Field(..., alias="editorialArtwork")
|
||||||
|
is_single: bool = Field(..., alias="isSingle")
|
||||||
|
name: str
|
||||||
|
artist_name: str = Field(..., alias="artistName")
|
||||||
|
content_rating: Optional[str] = Field(None, alias="contentRating")
|
||||||
|
is_complete: bool = Field(..., alias="isComplete")
|
||||||
|
|
||||||
|
|
||||||
|
class LastReleaseModel(BaseModel):
|
||||||
|
href: Optional[str] = None
|
||||||
|
attributes: Optional[AttributeName] = None
|
||||||
|
data: Optional[List[BaseDataModel[AttributeLastRelease]]] = None
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from shazamio.schemas.attributes import AttributeName
|
||||||
|
from shazamio.schemas.base import BaseHrefNextData
|
||||||
|
from shazamio.schemas.base import BaseIdTypeHref
|
||||||
|
from shazamio.schemas.photos import ImageModel
|
||||||
|
|
||||||
|
|
||||||
|
class EditorialArtwork(BaseModel):
|
||||||
|
centered_fullscreen_background: Optional[ImageModel] = Field(
|
||||||
|
None, alias="centeredFullscreenBackground"
|
||||||
|
)
|
||||||
|
subscription_hero: Optional[ImageModel] = Field(None, alias="subscriptionHero")
|
||||||
|
banner_uber: Optional[ImageModel] = Field(None, alias="bannerUber")
|
||||||
|
|
||||||
|
|
||||||
|
class Attributes(BaseModel):
|
||||||
|
genre_names: List[str] = Field(..., alias="genreNames")
|
||||||
|
editorial_artwork: EditorialArtwork = Field(..., alias="editorialArtwork")
|
||||||
|
name: str
|
||||||
|
artwork: ImageModel
|
||||||
|
url: str
|
||||||
|
origin: Optional[str] = None
|
||||||
|
artist_bio: Optional[str] = Field(None, alias="artistBio")
|
||||||
|
|
||||||
|
|
||||||
|
class Relationships(BaseModel):
|
||||||
|
albums: BaseHrefNextData[List[BaseIdTypeHref]]
|
||||||
|
|
||||||
|
|
||||||
|
class Datum(BaseModel):
|
||||||
|
id: str
|
||||||
|
type: str
|
||||||
|
href: str
|
||||||
|
attributes: Attributes
|
||||||
|
relationships: Relationships
|
||||||
|
|
||||||
|
|
||||||
|
class SimularArtist(BaseModel):
|
||||||
|
href: Optional[str] = None
|
||||||
|
next: Optional[str] = None
|
||||||
|
attributes: Optional[AttributeName] = None
|
||||||
|
data: Optional[List[Datum]] = None
|
||||||
45
lib/ShazamIO/shazamio/schemas/artist/views/top_music.py
Normal file
45
lib/ShazamIO/shazamio/schemas/artist/views/top_music.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from shazamio.schemas.attributes import AttributeName
|
||||||
|
from shazamio.schemas.base import BaseDataModel
|
||||||
|
from shazamio.schemas.photos import ImageModel
|
||||||
|
|
||||||
|
|
||||||
|
class PlayParams(BaseModel):
|
||||||
|
id: str
|
||||||
|
kind: str
|
||||||
|
|
||||||
|
|
||||||
|
class Preview(BaseModel):
|
||||||
|
url: str
|
||||||
|
hls_url: str = Field(..., alias="hlsUrl")
|
||||||
|
artwork: ImageModel
|
||||||
|
|
||||||
|
|
||||||
|
class Attributes(BaseModel):
|
||||||
|
genre_names: List[str] = Field(..., alias="genreNames")
|
||||||
|
release_date: str = Field(..., alias="releaseDate")
|
||||||
|
duration_in_millis: int = Field(..., alias="durationInMillis")
|
||||||
|
isrc: str
|
||||||
|
artwork: ImageModel
|
||||||
|
play_params: PlayParams = Field(..., alias="playParams")
|
||||||
|
url: str
|
||||||
|
has4_k: bool = Field(..., alias="has4K")
|
||||||
|
editorial_artwork: Dict[str, Any] = Field(..., alias="editorialArtwork")
|
||||||
|
has_hdr: bool = Field(..., alias="hasHDR")
|
||||||
|
name: str
|
||||||
|
previews: List[Preview]
|
||||||
|
artist_name: str = Field(..., alias="artistName")
|
||||||
|
content_rating: Optional[str] = Field(None, alias="contentRating")
|
||||||
|
album_name: Optional[str] = Field(None, alias="albumName")
|
||||||
|
track_number: Optional[int] = Field(None, alias="trackNumber")
|
||||||
|
|
||||||
|
|
||||||
|
class TopMusicVideosView(BaseModel):
|
||||||
|
href: Optional[str] = None
|
||||||
|
attributes: Optional[AttributeName] = None
|
||||||
|
data: Optional[List[BaseDataModel[Attributes]]] = None
|
||||||
44
lib/ShazamIO/shazamio/schemas/artist/views/top_song.py
Normal file
44
lib/ShazamIO/shazamio/schemas/artist/views/top_song.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from shazamio.schemas.artist.views.top_music import PlayParams
|
||||||
|
from shazamio.schemas.attributes import AttributeName
|
||||||
|
from shazamio.schemas.base import BaseDataModel
|
||||||
|
from shazamio.schemas.photos import ImageModel
|
||||||
|
from shazamio.schemas.urls import UrlDTO
|
||||||
|
|
||||||
|
|
||||||
|
class AttributesTopSong(BaseModel):
|
||||||
|
has_time_synced_lyrics: bool = Field(..., alias="hasTimeSyncedLyrics")
|
||||||
|
album_name: Optional[str] = Field(None, alias="albumName")
|
||||||
|
genre_names: List = Field(..., alias="genreNames")
|
||||||
|
track_number: int = Field(..., alias="trackNumber")
|
||||||
|
release_date: str = Field(..., alias="releaseDate")
|
||||||
|
duration_in_millis: int = Field(..., alias="durationInMillis")
|
||||||
|
is_vocal_attenuation_allowed: bool = Field(..., alias="isVocalAttenuationAllowed")
|
||||||
|
is_mastered_for_itunes: bool = Field(..., alias="isMasteredForItunes")
|
||||||
|
isrc: str
|
||||||
|
artwork: ImageModel
|
||||||
|
composer_name: str = Field(..., alias="composerName")
|
||||||
|
audio_locale: str = Field(..., alias="audioLocale")
|
||||||
|
url: str
|
||||||
|
play_params: PlayParams = Field(..., alias="playParams")
|
||||||
|
disc_number: int = Field(..., alias="discNumber")
|
||||||
|
has_lyrics: bool = Field(..., alias="hasLyrics")
|
||||||
|
is_apple_digital_master: bool = Field(..., alias="isAppleDigitalMaster")
|
||||||
|
audio_traits: List[str] = Field(..., alias="audioTraits")
|
||||||
|
name: str
|
||||||
|
previews: List[UrlDTO] = Field([])
|
||||||
|
artist_name: str = Field(..., alias="artistName")
|
||||||
|
content_rating: Optional[str] = Field(None, alias="contentRating")
|
||||||
|
|
||||||
|
|
||||||
|
class TopSong(BaseModel):
|
||||||
|
id: Optional[str] = None
|
||||||
|
type: Optional[str] = None
|
||||||
|
href: Optional[str] = None
|
||||||
|
attributes: Optional[AttributeName] = None
|
||||||
|
data: Optional[List[BaseDataModel[AttributesTopSong]]] = None
|
||||||
100
lib/ShazamIO/shazamio/schemas/artists.py
Normal file
100
lib/ShazamIO/shazamio/schemas/artists.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from dataclasses import field
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from shazamio.schemas.artist.views.full_albums import FullAlbumsModel
|
||||||
|
from shazamio.schemas.artist.views.last_release import LastReleaseModel
|
||||||
|
from shazamio.schemas.artist.views.simular_artists import SimularArtist
|
||||||
|
from shazamio.schemas.artist.views.top_music import TopMusicVideosView
|
||||||
|
from shazamio.schemas.artist.views.top_song import TopSong
|
||||||
|
from shazamio.schemas.attributes import ArtistAttribute
|
||||||
|
from shazamio.schemas.enums import ArtistExtend
|
||||||
|
from shazamio.schemas.enums import ArtistView
|
||||||
|
from shazamio.schemas.errors import ErrorModel
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ArtistInfo:
|
||||||
|
name: str
|
||||||
|
verified: Optional[bool]
|
||||||
|
genres: Optional[List[str]] = field(default_factory=list)
|
||||||
|
alias: Optional[str] = None
|
||||||
|
genres_primary: Optional[str] = None
|
||||||
|
avatar: Optional[Union[dict, str]] = None
|
||||||
|
adam_id: Optional[int] = None
|
||||||
|
url: Optional[str] = ""
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.avatar = self.__optional_avatar()
|
||||||
|
|
||||||
|
def __optional_avatar(self) -> Optional[str]:
|
||||||
|
if self.avatar is None:
|
||||||
|
return None
|
||||||
|
elif "default" in self.avatar:
|
||||||
|
return self.avatar.get("default")
|
||||||
|
else:
|
||||||
|
return "".join(self.avatar)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ArtistV2:
|
||||||
|
artist: ArtistInfo
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ArtistQuery:
|
||||||
|
views: List[ArtistView] = field(default_factory=list)
|
||||||
|
extend: List[ArtistExtend] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ArtistAvatar:
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
url: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def url_with_size(cls, height: int, width: int) -> str:
|
||||||
|
return cls.url.format(w=width, h=height)
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumRelationshipElement(BaseModel):
|
||||||
|
id: str
|
||||||
|
type: str
|
||||||
|
href: str
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumRelationship(BaseModel):
|
||||||
|
href: str
|
||||||
|
next: Optional[str] = None
|
||||||
|
data: List[AlbumRelationshipElement]
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistRelationships(BaseModel):
|
||||||
|
albums: AlbumRelationship
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistViews(BaseModel):
|
||||||
|
top_music_videos: Optional[TopMusicVideosView] = Field(None, alias="top-music-videos")
|
||||||
|
simular_artists: Optional[SimularArtist] = Field(None, alias="similar-artists")
|
||||||
|
latest_release: Optional[LastReleaseModel] = Field(None, alias="latest-release")
|
||||||
|
full_albums: Optional[FullAlbumsModel] = Field(None, alias="full-albums")
|
||||||
|
top_songs: Optional[TopSong] = Field(None, alias="top-songs")
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistV3(BaseModel):
|
||||||
|
id: str
|
||||||
|
type: str
|
||||||
|
attributes: ArtistAttribute
|
||||||
|
relationships: ArtistRelationships
|
||||||
|
views: ArtistViews
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistResponse(BaseModel):
|
||||||
|
errors: List[ErrorModel] = []
|
||||||
|
data: List[ArtistV3] = []
|
||||||
16
lib/ShazamIO/shazamio/schemas/attributes.py
Normal file
16
lib/ShazamIO/shazamio/schemas/attributes.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeName(BaseModel):
|
||||||
|
title: str
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistAttribute(BaseModel):
|
||||||
|
genre_names: List[str] = Field([], alias="genreNames")
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
artist_bio: Optional[str] = Field(None, alias="artistBio")
|
||||||
25
lib/ShazamIO/shazamio/schemas/base.py
Normal file
25
lib/ShazamIO/shazamio/schemas/base.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from typing import Generic
|
||||||
|
from typing import Optional
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from pydantic.generics import GenericModel
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseIdTypeHref(BaseModel):
|
||||||
|
id: str
|
||||||
|
type: str
|
||||||
|
href: str
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDataModel(GenericModel, BaseModel, Generic[T]):
|
||||||
|
attributes: T
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHrefNextData(GenericModel, Generic[T]):
|
||||||
|
href: str
|
||||||
|
next: Optional[str] = None
|
||||||
|
data: T
|
||||||
18
lib/ShazamIO/shazamio/schemas/enums.py
Normal file
18
lib/ShazamIO/shazamio/schemas/enums.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistExtend(str, Enum):
|
||||||
|
ARTIST_BIO = "artistBio"
|
||||||
|
BORN_OF_FORMED = "bornOrFormed"
|
||||||
|
EDITORIAL_ARTWORK = "editorialArtwork"
|
||||||
|
ORIGIN = "origin"
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistView(str, Enum):
|
||||||
|
FULL_ALBUMS = "full-albums"
|
||||||
|
FEATURED_ALBUMS = "featured-albums"
|
||||||
|
LATEST_RELEASE = "latest-release"
|
||||||
|
TOP_MUSIC_VIDEOS = "top-music-videos"
|
||||||
|
SIMULAR_ARTISTS = "similar-artists"
|
||||||
|
TOP_SONGS = "top-songs"
|
||||||
|
PLAYLISTS = "playlists"
|
||||||
10
lib/ShazamIO/shazamio/schemas/errors.py
Normal file
10
lib/ShazamIO/shazamio/schemas/errors.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ErrorModel:
|
||||||
|
id: str
|
||||||
|
title: str
|
||||||
|
detail: str
|
||||||
|
status: str
|
||||||
|
code: str
|
||||||
201
lib/ShazamIO/shazamio/schemas/models.py
Normal file
201
lib/ShazamIO/shazamio/schemas/models.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from dataclasses import field
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Union
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from urllib.parse import urlunparse
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ShareModel:
|
||||||
|
subject: str
|
||||||
|
text: str
|
||||||
|
href: str
|
||||||
|
image: str
|
||||||
|
twitter: str
|
||||||
|
html: str
|
||||||
|
snapchat: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ActionModel:
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
share: ShareModel
|
||||||
|
uri: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SongMetaPages:
|
||||||
|
image: str
|
||||||
|
caption: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SongMetadata:
|
||||||
|
title: str
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SongSection:
|
||||||
|
type: str
|
||||||
|
meta_pages: List[SongMetaPages]
|
||||||
|
tab_name: str
|
||||||
|
metadata: List[SongMetadata]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BaseIdTypeModel:
|
||||||
|
type: str
|
||||||
|
id: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TopTracksModel:
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ArtistSection:
|
||||||
|
type: str
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
verified: bool
|
||||||
|
actions: List[BaseIdTypeModel]
|
||||||
|
tab_name: str
|
||||||
|
top_tracks: TopTracksModel
|
||||||
|
|
||||||
|
|
||||||
|
class BeaconDataLyricsSection:
|
||||||
|
lyrics_id: str
|
||||||
|
provider_name: str
|
||||||
|
common_track_id: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LyricsSection:
|
||||||
|
type: str
|
||||||
|
text: List[str]
|
||||||
|
footer: str
|
||||||
|
tab_name: str
|
||||||
|
beacon_data: Optional[BeaconDataLyricsSection]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VideoSection:
|
||||||
|
tab_name: str
|
||||||
|
youtube_url: str
|
||||||
|
type: str = "VIDEO"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RelatedSection:
|
||||||
|
type: str
|
||||||
|
url: str
|
||||||
|
tab_name: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DimensionsModel:
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class YoutubeImageModel:
|
||||||
|
dimensions: DimensionsModel
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MatchModel:
|
||||||
|
id: str
|
||||||
|
offset: float
|
||||||
|
time_skew: float
|
||||||
|
frequency_skew: float
|
||||||
|
channel: Optional[str] = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LocationModel:
|
||||||
|
accuracy: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class YoutubeData:
|
||||||
|
caption: str
|
||||||
|
image: YoutubeImageModel
|
||||||
|
actions: List[ActionModel]
|
||||||
|
uri: Optional[str] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.uri = self.__get_youtube_uri()
|
||||||
|
|
||||||
|
def __get_youtube_uri(self):
|
||||||
|
if self.actions:
|
||||||
|
for action in self.actions:
|
||||||
|
if action.uri:
|
||||||
|
return action.uri
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TrackInfo:
|
||||||
|
key: int
|
||||||
|
title: str
|
||||||
|
subtitle: str
|
||||||
|
artist_id: Optional[str] = field(default=None)
|
||||||
|
shazam_url: str = None
|
||||||
|
photo_url: Optional[str] = field(init=False, default=None)
|
||||||
|
spotify_uri_query: Optional[str] = None
|
||||||
|
apple_music_url: Optional[str] = None
|
||||||
|
ringtone: Optional[str] = None
|
||||||
|
spotify_url: Optional[str] = field(default=None)
|
||||||
|
spotify_uri: Optional[str] = field(default=None)
|
||||||
|
youtube_link: Optional[str] = None
|
||||||
|
sections: Optional[
|
||||||
|
List[
|
||||||
|
Union[
|
||||||
|
SongSection,
|
||||||
|
VideoSection,
|
||||||
|
RelatedSection,
|
||||||
|
ArtistSection,
|
||||||
|
LyricsSection,
|
||||||
|
]
|
||||||
|
]
|
||||||
|
] = field(default_factory=list)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.shazam_url = f"https://www.shazam.com/track/{self.artist_id}"
|
||||||
|
self.apple_music_url = self.__apple_music_url()
|
||||||
|
self.spotify_uri_query = self.__short_uri()
|
||||||
|
self.youtube_link = self.__youtube_link()
|
||||||
|
|
||||||
|
def __apple_music_url(self):
|
||||||
|
url_parse_list = list(urlparse(self.apple_music_url))
|
||||||
|
url_parse_list[4] = urlencode({}, doseq=True)
|
||||||
|
url_deleted_query = urlunparse(url_parse_list)
|
||||||
|
return url_deleted_query
|
||||||
|
|
||||||
|
def __short_uri(self):
|
||||||
|
if self.spotify_uri:
|
||||||
|
return self.spotify_uri.split("spotify:search:")[1]
|
||||||
|
|
||||||
|
def __youtube_link(self):
|
||||||
|
for i in self.sections:
|
||||||
|
if type(i) is VideoSection:
|
||||||
|
return i.youtube_url
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ResponseTrack:
|
||||||
|
tag_id: Optional[UUID]
|
||||||
|
retry_ms: Optional[int] = field(default=None)
|
||||||
|
location: Optional[LocationModel] = field(default=None)
|
||||||
|
matches: List[MatchModel] = field(default_factory=list)
|
||||||
|
timestamp: Optional[int] = field(default=None)
|
||||||
|
timezone: Optional[str] = field(default=None)
|
||||||
|
track: Optional[TrackInfo] = field(default=None)
|
||||||
16
lib/ShazamIO/shazamio/schemas/photos.py
Normal file
16
lib/ShazamIO/shazamio/schemas/photos.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
|
||||||
|
class ImageModel(BaseModel):
|
||||||
|
width: int
|
||||||
|
url: str
|
||||||
|
height: int
|
||||||
|
text_color3: Optional[str] = Field(None, alias="textColor3")
|
||||||
|
text_color2: Optional[str] = Field(None, alias="textColor2")
|
||||||
|
text_color4: Optional[str] = Field(None, alias="textColor4")
|
||||||
|
text_color1: Optional[str] = Field(None, alias="textColor1")
|
||||||
|
bg_color: Optional[str] = Field(None, alias="bgColor")
|
||||||
|
has_p3: bool = Field(..., alias="hasP3")
|
||||||
5
lib/ShazamIO/shazamio/schemas/urls.py
Normal file
5
lib/ShazamIO/shazamio/schemas/urls.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class UrlDTO(BaseModel):
|
||||||
|
url: str
|
||||||
32
lib/ShazamIO/shazamio/serializers.py
Normal file
32
lib/ShazamIO/shazamio/serializers.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from shazamio.factory_misc import FACTORY_ARTIST
|
||||||
|
from shazamio.factory_misc import FACTORY_TRACK
|
||||||
|
from shazamio.schemas.artists import ArtistInfo
|
||||||
|
from shazamio.schemas.artists import ArtistResponse
|
||||||
|
from shazamio.schemas.artists import ArtistV2
|
||||||
|
from shazamio.schemas.models import ResponseTrack
|
||||||
|
from shazamio.schemas.models import TrackInfo
|
||||||
|
from shazamio.schemas.models import YoutubeData
|
||||||
|
|
||||||
|
|
||||||
|
class Serialize:
|
||||||
|
@classmethod
|
||||||
|
def track(cls, data):
|
||||||
|
return FACTORY_TRACK.load(data, TrackInfo)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def youtube(cls, data):
|
||||||
|
return FACTORY_TRACK.load(data, YoutubeData)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def artist_v2(cls, data) -> ArtistResponse:
|
||||||
|
return ArtistResponse.parse_obj(data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def artist(cls, data):
|
||||||
|
return FACTORY_ARTIST.load(data, Union[ArtistV2, ArtistInfo])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def full_track(cls, data):
|
||||||
|
return FACTORY_TRACK.load(data, ResponseTrack)
|
||||||
263
lib/ShazamIO/shazamio/signature.py
Normal file
263
lib/ShazamIO/shazamio/signature.py
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
from typing import Dict, List
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
from math import exp, sqrt
|
||||||
|
from binascii import crc32
|
||||||
|
from io import BytesIO
|
||||||
|
from ctypes import *
|
||||||
|
from .enums import FrequencyBand, SampleRate
|
||||||
|
|
||||||
|
DATA_URI_PREFIX = "data:audio/vnd.shazam.sig;base64,"
|
||||||
|
|
||||||
|
|
||||||
|
class RawSignatureHeader(LittleEndianStructure):
|
||||||
|
_pack = True
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
("magic1", c_uint32), # Fixed 0xcafe2580 - 80 25 fe ca
|
||||||
|
(
|
||||||
|
"crc32",
|
||||||
|
c_uint32,
|
||||||
|
), # CRC-32 for all following (so excluding these first 8 bytes)
|
||||||
|
("size_minus_header", c_uint32),
|
||||||
|
# Total size of the message, minus the size of the current header (which is 48 bytes)
|
||||||
|
("magic2", c_uint32), # Fixed 0x94119c00 - 00 9c 11 94
|
||||||
|
("void1", c_uint32 * 3), # Void
|
||||||
|
("shifted_sample_rate_id", c_uint32),
|
||||||
|
# A member of SampleRate (usually 3 for 16000 Hz), left-shifted by 27 (usually giving
|
||||||
|
# 0x18000000 - 00 00 00 18)
|
||||||
|
("void2", c_uint32 * 2), # Void, or maybe used only in "rolling window" mode?
|
||||||
|
("number_samples_plus_divided_sample_rate", c_uint32),
|
||||||
|
# int(number_of_samples + sample_rate * 0.24) - As the sample rate is known thanks to the
|
||||||
|
# field above,
|
||||||
|
# it can be inferred and subtracted so that we obtain the number of samples,
|
||||||
|
# and from the number of samples and sample rate we can obtain the length of the recording
|
||||||
|
("fixed_value", c_uint32)
|
||||||
|
# Calculated as ((15 << 19) + 0x40000) - 0x7c0000 or 00 00 7c 00 - seems pretty constant,
|
||||||
|
# may be different in the "SigType.STREAMING" mode
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class FrequencyPeak:
|
||||||
|
fft_pass_number: int = None
|
||||||
|
peak_magnitude: int = None
|
||||||
|
corrected_peak_frequency_bin: int = None
|
||||||
|
sample_rate_hz: int = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
fft_pass_number: int,
|
||||||
|
peak_magnitude: int,
|
||||||
|
corrected_peak_frequency_bin: int,
|
||||||
|
sample_rate_hz: int,
|
||||||
|
):
|
||||||
|
self.fft_pass_number = fft_pass_number
|
||||||
|
self.peak_magnitude = peak_magnitude
|
||||||
|
self.corrected_peak_frequency_bin = corrected_peak_frequency_bin
|
||||||
|
self.sample_rate_hz = sample_rate_hz
|
||||||
|
|
||||||
|
def get_frequency_hz(self) -> float:
|
||||||
|
return self.corrected_peak_frequency_bin * (self.sample_rate_hz / 2 / 1024 / 64)
|
||||||
|
|
||||||
|
# ^ Convert back FFT bin to a frequency, given a 16 KHz sample
|
||||||
|
# rate, 1024 useful bins and the multiplication by 64 made before
|
||||||
|
# storing the information
|
||||||
|
|
||||||
|
def get_amplitude_pcm(self) -> float:
|
||||||
|
return sqrt(exp((self.peak_magnitude - 6144) / 1477.3) * (1 << 17) / 2) / 1024
|
||||||
|
|
||||||
|
# ^ Not sure about this calculation but gives small enough numbers
|
||||||
|
|
||||||
|
def get_seconds(self) -> float:
|
||||||
|
return (self.fft_pass_number * 128) / self.sample_rate_hz
|
||||||
|
|
||||||
|
# ^ Assume that new FFT bins are emitted every 128 samples, on a
|
||||||
|
# standard 16 KHz sample rate basis.
|
||||||
|
|
||||||
|
|
||||||
|
class DecodedMessage:
|
||||||
|
sample_rate_hz: int = None
|
||||||
|
number_samples: int = None
|
||||||
|
|
||||||
|
frequency_band_to_sound_peaks: Dict[FrequencyBand, List[FrequencyPeak]] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def decode_from_binary(cls, data: bytes):
|
||||||
|
self = cls()
|
||||||
|
|
||||||
|
buf = BytesIO(data)
|
||||||
|
|
||||||
|
buf.seek(8)
|
||||||
|
check_summable_data = buf.read()
|
||||||
|
buf.seek(0)
|
||||||
|
|
||||||
|
# Read and check the header
|
||||||
|
|
||||||
|
header = RawSignatureHeader()
|
||||||
|
buf.readinto(header)
|
||||||
|
|
||||||
|
assert header.magic1 == 0xCAFE2580
|
||||||
|
assert header.size_minus_header == len(data) - 48
|
||||||
|
assert crc32(check_summable_data) & 0xFFFFFFFF == header.crc32
|
||||||
|
assert header.magic2 == 0x94119C00
|
||||||
|
|
||||||
|
self.sample_rate_hz = int(SampleRate(header.shifted_sample_rate_id >> 27).name.strip("_"))
|
||||||
|
|
||||||
|
self.number_samples = int(
|
||||||
|
header.number_samples_plus_divided_sample_rate - self.sample_rate_hz * 0.24
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read the type-length-value sequence that follows the header
|
||||||
|
|
||||||
|
# The first chunk is fixed and has no value, but instead just repeats
|
||||||
|
# the length of the message size minus the header:
|
||||||
|
assert int.from_bytes(buf.read(4), "little") == 0x40000000
|
||||||
|
assert int.from_bytes(buf.read(4), "little") == len(data) - 48
|
||||||
|
|
||||||
|
# Then, lists of frequency peaks for respective bands follow
|
||||||
|
|
||||||
|
self.frequency_band_to_sound_peaks = {}
|
||||||
|
|
||||||
|
while True:
|
||||||
|
tlv_header = buf.read(8)
|
||||||
|
if not tlv_header:
|
||||||
|
break
|
||||||
|
|
||||||
|
frequency_band_id = int.from_bytes(tlv_header[:4], "little")
|
||||||
|
frequency_peaks_size = int.from_bytes(tlv_header[4:], "little")
|
||||||
|
|
||||||
|
frequency_peaks_padding = -frequency_peaks_size % 4
|
||||||
|
|
||||||
|
frequency_peaks_buf = BytesIO(buf.read(frequency_peaks_size))
|
||||||
|
buf.read(frequency_peaks_padding)
|
||||||
|
|
||||||
|
# Decode frequency peaks
|
||||||
|
|
||||||
|
frequency_band = FrequencyBand(frequency_band_id - 0x60030040)
|
||||||
|
|
||||||
|
fft_pass_number = 0
|
||||||
|
|
||||||
|
self.frequency_band_to_sound_peaks[frequency_band] = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
raw_fft_pass: bytes = frequency_peaks_buf.read(1)
|
||||||
|
if not raw_fft_pass:
|
||||||
|
break
|
||||||
|
|
||||||
|
fft_pass_offset: int = raw_fft_pass[0]
|
||||||
|
if fft_pass_offset == 0xFF:
|
||||||
|
fft_pass_number = int.from_bytes(frequency_peaks_buf.read(4), "little")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
fft_pass_number += fft_pass_offset
|
||||||
|
|
||||||
|
peak_magnitude = int.from_bytes(frequency_peaks_buf.read(2), "little")
|
||||||
|
corrected_peak_frequency_bin = int.from_bytes(frequency_peaks_buf.read(2), "little")
|
||||||
|
|
||||||
|
self.frequency_band_to_sound_peaks[frequency_band].append(
|
||||||
|
FrequencyPeak(
|
||||||
|
fft_pass_number,
|
||||||
|
peak_magnitude,
|
||||||
|
corrected_peak_frequency_bin,
|
||||||
|
self.sample_rate_hz,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def decode_from_uri(cls, uri: str):
|
||||||
|
assert uri.startswith(DATA_URI_PREFIX)
|
||||||
|
|
||||||
|
return cls.decode_from_binary(b64decode(uri.replace(DATA_URI_PREFIX, "", 1)))
|
||||||
|
|
||||||
|
"""
|
||||||
|
Encode the current object to a readable JSON format, for debugging
|
||||||
|
purposes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def encode_to_json(self) -> dict:
|
||||||
|
return {
|
||||||
|
"sample_rate_hz": self.sample_rate_hz,
|
||||||
|
"number_samples": self.number_samples,
|
||||||
|
"_seconds": self.number_samples / self.sample_rate_hz,
|
||||||
|
"frequency_band_to_peaks": {
|
||||||
|
frequency_band.name.strip("_"): [
|
||||||
|
{
|
||||||
|
"fft_pass_number": frequency_peak.fft_pass_number,
|
||||||
|
"peak_magnitude": frequency_peak.peak_magnitude,
|
||||||
|
"corrected_peak_frequency_bin": frequency_peak.corrected_peak_frequency_bin,
|
||||||
|
"_frequency_hz": frequency_peak.get_frequency_hz(),
|
||||||
|
"_amplitude_pcm": frequency_peak.get_amplitude_pcm(),
|
||||||
|
"_seconds": frequency_peak.get_seconds(),
|
||||||
|
}
|
||||||
|
for frequency_peak in frequency_peaks
|
||||||
|
]
|
||||||
|
for frequency_band, frequency_peaks in sorted(
|
||||||
|
self.frequency_band_to_sound_peaks.items()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def encode_to_binary(self) -> bytes:
|
||||||
|
header = RawSignatureHeader()
|
||||||
|
|
||||||
|
header.magic1 = 0xCAFE2580
|
||||||
|
header.magic2 = 0x94119C00
|
||||||
|
header.shifted_sample_rate_id = int(getattr(SampleRate, "_%s" % self.sample_rate_hz)) << 27
|
||||||
|
header.fixed_value = (15 << 19) + 0x40000
|
||||||
|
header.number_samples_plus_divided_sample_rate = int(
|
||||||
|
self.number_samples + self.sample_rate_hz * 0.24
|
||||||
|
)
|
||||||
|
|
||||||
|
contents_buf = BytesIO()
|
||||||
|
|
||||||
|
for frequency_band, frequency_peaks in sorted(self.frequency_band_to_sound_peaks.items()):
|
||||||
|
peaks_buf = BytesIO()
|
||||||
|
|
||||||
|
fft_pass_number = 0
|
||||||
|
|
||||||
|
# NOTE: Correctly filtering and sorting the peaks within the members
|
||||||
|
# of "self.frequency_band_to_sound_peaks" is the responsibility of the
|
||||||
|
# caller
|
||||||
|
|
||||||
|
for frequency_peak in frequency_peaks:
|
||||||
|
assert frequency_peak.fft_pass_number >= fft_pass_number
|
||||||
|
|
||||||
|
if frequency_peak.fft_pass_number - fft_pass_number >= 255:
|
||||||
|
peaks_buf.write(b"\xff")
|
||||||
|
peaks_buf.write(frequency_peak.fft_pass_number.to_bytes(4, "little"))
|
||||||
|
|
||||||
|
fft_pass_number = frequency_peak.fft_pass_number
|
||||||
|
|
||||||
|
peaks_buf.write(bytes([frequency_peak.fft_pass_number - fft_pass_number]))
|
||||||
|
peaks_buf.write(frequency_peak.peak_magnitude.to_bytes(2, "little"))
|
||||||
|
peaks_buf.write(frequency_peak.corrected_peak_frequency_bin.to_bytes(2, "little"))
|
||||||
|
|
||||||
|
fft_pass_number = frequency_peak.fft_pass_number
|
||||||
|
|
||||||
|
contents_buf.write((0x60030040 + int(frequency_band)).to_bytes(4, "little"))
|
||||||
|
contents_buf.write(len(peaks_buf.getvalue()).to_bytes(4, "little"))
|
||||||
|
contents_buf.write(peaks_buf.getvalue())
|
||||||
|
contents_buf.write(b"\x00" * (-len(peaks_buf.getvalue()) % 4))
|
||||||
|
|
||||||
|
# Below, write the full message as a binary stream
|
||||||
|
|
||||||
|
header.size_minus_header = len(contents_buf.getvalue()) + 8
|
||||||
|
|
||||||
|
buf = BytesIO()
|
||||||
|
buf.write(header) # We will rewrite it just after in order to include the final CRC-32
|
||||||
|
|
||||||
|
buf.write((0x40000000).to_bytes(4, "little"))
|
||||||
|
buf.write((len(contents_buf.getvalue()) + 8).to_bytes(4, "little"))
|
||||||
|
|
||||||
|
buf.write(contents_buf.getvalue())
|
||||||
|
|
||||||
|
buf.seek(8)
|
||||||
|
header.crc32 = crc32(buf.read()) & 0xFFFFFFFF
|
||||||
|
buf.seek(0)
|
||||||
|
buf.write(header)
|
||||||
|
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
def encode_to_uri(self) -> str:
|
||||||
|
return DATA_URI_PREFIX + b64encode(self.encode_to_binary()).decode("ascii")
|
||||||
10
lib/ShazamIO/shazamio/typehints.py
Normal file
10
lib/ShazamIO/shazamio/typehints.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class CountryCode:
|
||||||
|
"""ISO 3166-3 alpha-2 code. Example: RU,NL,UA"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ShazamResponse:
|
||||||
|
"""Dictionary with found data on request"""
|
||||||
|
|
||||||
|
pass
|
||||||
102
lib/ShazamIO/shazamio/user_agent.py
Normal file
102
lib/ShazamIO/shazamio/user_agent.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
USER_AGENTS = [
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0.2; VS980 4G Build/LRX22G)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-T210 Build/KOT49H)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-P905V Build/LMY47X)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; Vodafone Smart Tab 4G Build/KTU84P)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; SM-G360H Build/KTU84P)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0.2; SM-S920L Build/LRX22G)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0; Fire Pro Build/LRX21M)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0; SM-N9005 Build/LRX21V)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G920F Build/MMB29K)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G7102 Build/KOT49H)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0; SM-G900F Build/LRX21T)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G928F Build/MMB29K)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-J500FN Build/LMY48B)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; Coolpad 3320A Build/LMY47V)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; SM-J110F Build/KTU84P)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; SAMSUNG-SGH-I747 Build/KOT49H)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; SAMSUNG-SM-T337A Build/KOT49H)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.3; SGH-T999 Build/JSS15J)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; D6603 Build/23.5.A.0.570)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-J700H Build/LMY48B)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; HTC6600LVW Build/KOT49H)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-N910G Build/LMY47X)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-N910T Build/LMY47X)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; C6903 Build/14.4.A.0.157)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G920F Build/MMB29K)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.2.2; GT-I9105P Build/JDQ39)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0; SM-G900F Build/LRX21T)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-I9192 Build/KOT49H)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-G531H Build/LMY48B)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0; SM-N9005 Build/LRX21V)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; LGMS345 Build/LMY47V)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0.2; HTC One Build/LRX22G)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0.2; LG-D800 Build/LRX22G)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-G531H Build/LMY48B)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0; SM-N9005 Build/LRX21V)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; SM-T113 Build/KTU84P)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.2.2; AndyWin Build/JDQ39E)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0; Lenovo A7000-a Build/LRX21M)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; LGL16C Build/KOT49I.L16CV11a)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-I9500 Build/KOT49H)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0.2; SM-A700FD Build/LRX22G)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G130HN Build/KOT49H)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-N9005 Build/KOT49H)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.1.2; LG-E975T Build/JZO54K)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; E1 Build/KOT49H)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-I9500 Build/KOT49H)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-N5100 Build/KOT49H)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-A310F Build/LMY47X)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-J105H Build/LMY47V)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.3; GT-I9305T Build/JSS15J)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; android Build/JDQ39)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.2.1; HS-U970 Build/JOP40D)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; SM-T561 Build/KTU84P)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.2.2; GT-P3110 Build/JDQ39)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G925T Build/MMB29K)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; HUAWEI Y221-U22 Build/HUAWEIY221-U22)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-G530T1 Build/LMY47X)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-G920I Build/LMY47X)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0; SM-G900F Build/LRX21T)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; Vodafone Smart ultra 6 Build/LMY47V)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; XT1080 Build/SU6-7.7)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; ASUS MeMO Pad 7 Build/KTU84P)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-G800F Build/KOT49H)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-N7100 Build/KOT49H)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G925I Build/MMB29K)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; A0001 Build/MMB29X)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1; XT1045 Build/LPB23.13-61)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; LGMS330 Build/LMY47V)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; Z970 Build/KTU84P)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0; SM-N900P Build/LRX21V)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; T1-701u Build/HuaweiMediaPad)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1; HTCD100LVWPP Build/LMY47O)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G935R4 Build/MMB29M)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G930V Build/MMB29M)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0.2; ZTE Blade Q Lux Build/LRX22G)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; GT-I9060I Build/KTU84P)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; LGUS992 Build/MMB29M)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G900P Build/MMB29M)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.1.2; SGH-T999L Build/JZO54K)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-N910V Build/LMY47X)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; GT-I9500 Build/KOT49H)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-P601 Build/LMY47X)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.2.2; GT-S7272 Build/JDQ39)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-N910T Build/LMY47X)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.3; SAMSUNG-SGH-I747 Build/JSS15J)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0.2; ZTE Blade Q Lux Build/LRX22G)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G930F Build/MMB29K)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; HTC_PO582 Build/KOT49H)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0; HUAWEI MT7-TL10 Build/HuaweiMT7-TL10)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0; LG-H811 Build/MRA58K)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-N7505 Build/KOT49H)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0; LG-H815 Build/MRA58K)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.2; LenovoA3300-HV Build/KOT49H)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; SM-G360G Build/KTU84P)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; GT-I9300I Build/KTU84P)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.0; SM-G900F Build/LRX21T)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-J700T Build/MMB29K)",
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-J500FN Build/LMY48B)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.2.2; SM-T217S Build/JDQ39)",
|
||||||
|
"Dalvik/1.6.0 (Linux; U; Android 4.4.4; SAMSUNG-SM-N900A Build/KTU84P)",
|
||||||
|
]
|
||||||
70
lib/ShazamIO/shazamio/utils.py
Normal file
70
lib/ShazamIO/shazamio/utils.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import pathlib
|
||||||
|
from enum import Enum
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Dict
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
import aiohttp
|
||||||
|
from aiohttp import ContentTypeError
|
||||||
|
from pydub import AudioSegment
|
||||||
|
|
||||||
|
from shazamio.exceptions import FailedDecodeJson
|
||||||
|
from shazamio.schemas.artists import ArtistQuery
|
||||||
|
|
||||||
|
SongT = Union[str, pathlib.Path, bytes, bytearray]
|
||||||
|
FileT = Union[str, pathlib.Path]
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_json(
|
||||||
|
resp: aiohttp.ClientResponse, content_type: str = "application/json"
|
||||||
|
) -> dict:
|
||||||
|
try:
|
||||||
|
return await resp.json(content_type=content_type)
|
||||||
|
except ContentTypeError as e:
|
||||||
|
bad_url = str(str(e).split(",")[2]).split("'")[1]
|
||||||
|
raise FailedDecodeJson(f"Check args, URL is invalid\nURL- {bad_url}")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_file_bytes(file: FileT) -> bytes:
|
||||||
|
async with aiofiles.open(file, mode="rb") as f:
|
||||||
|
return await f.read()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_song(data: SongT) -> Union[AudioSegment]:
|
||||||
|
if isinstance(data, (str, pathlib.Path)):
|
||||||
|
song_bytes = await get_file_bytes(file=data)
|
||||||
|
return AudioSegment.from_file(BytesIO(song_bytes))
|
||||||
|
|
||||||
|
if isinstance(data, (bytes, bytearray)):
|
||||||
|
return AudioSegment.from_file(BytesIO(data))
|
||||||
|
|
||||||
|
if isinstance(data, AudioSegment):
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class QueryBuilder:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
source: List[Union[str, Enum]],
|
||||||
|
):
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
return ",".join(self.source)
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistQueryGenerator:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
source: Optional[ArtistQuery] = None,
|
||||||
|
):
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
def params(self) -> Dict[str, str]:
|
||||||
|
return {
|
||||||
|
"extend": QueryBuilder(source=self.source.extend or []).to_str(),
|
||||||
|
"views": QueryBuilder(source=self.source.views or []).to_str(),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user