Skip to content
Merged
1 change: 1 addition & 0 deletions scratchattach/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .other.other_apis import *
from .other.project_json_capabilities import ProjectBody, get_empty_project_pb, get_pb_from_dict, read_sb3_file, download_asset
from .utils.encoder import Encoding
from .utils.supportedlangs import Languages

from .site.activity import Activity
from .site.backpack_asset import BackpackAsset
Expand Down
100 changes: 97 additions & 3 deletions scratchattach/other/other_apis.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,94 @@
"""Other Scratch API-related functions"""

import json
import warnings

from ..utils import commons
from ..utils.exceptions import BadRequest
from ..utils.requests import Requests as requests
import json
from ..utils.supportedlangs import Languages


# --- Front page ---

def get_news(*, limit=10, offset=0):
return commons.api_iterative("https://api.scratch.mit.edu/news", limit = limit, offset = offset)
return commons.api_iterative("https://api.scratch.mit.edu/news", limit=limit, offset=offset)


def featured_data():
return requests.get("https://api.scratch.mit.edu/proxy/featured").json()


def featured_projects():
return featured_data()["community_featured_projects"]


def featured_studios():
return featured_data()["community_featured_studios"]


def top_loved():
return featured_data()["community_most_loved_projects"]


def top_remixed():
return featured_data()["community_most_remixed_projects"]


def newest_projects():
return featured_data()["community_newest_projects"]


def curated_projects():
return featured_data()["curator_top_projects"]


def design_studio_projects():
return featured_data()["scratch_design_studio"]


# --- Statistics ---

def total_site_stats():
data = requests.get("https://scratch.mit.edu/statistics/data/daily/").json()
data.pop("_TS")
return data


def monthly_site_traffic():
data = requests.get("https://scratch.mit.edu/statistics/data/monthly-ga/").json()
data.pop("_TS")
return data


def country_counts():
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["country_distribution"]


def age_distribution():
data = requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["age_distribution_data"][0]["values"]
return_data = {}
for value in data:
return_data[value["x"]] = value["y"]
return return_data


def monthly_comment_activity():
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["comment_data"]


def monthly_project_shares():
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["project_data"]


def monthly_active_users():
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["active_user_data"]


def monthly_activity_trends():
return requests.get("https://scratch.mit.edu/statistics/data/monthly/").json()["activity_data"]


# --- CSRF Token Generation API ---

def get_csrf_token():
Expand All @@ -80,32 +102,41 @@ def get_csrf_token():
"https://scratch.mit.edu/csrf_token/"
).headers["set-cookie"].split(";")[3][len(" Path=/, scratchcsrftoken="):]


# --- Various other api.scratch.mit.edu API endpoints ---

def get_health():
return requests.get("https://api.scratch.mit.edu/health").json()


def get_total_project_count() -> int:
return requests.get("https://api.scratch.mit.edu/projects/count/all").json()["count"]


def check_username(username):
return requests.get(f"https://api.scratch.mit.edu/accounts/checkusername/{username}").json()["msg"]


def check_password(password):
return requests.post("https://api.scratch.mit.edu/accounts/checkpassword/", json={"password":password}).json()["msg"]
return requests.post("https://api.scratch.mit.edu/accounts/checkpassword/", json={"password": password}).json()[
"msg"]


# --- April fools endpoints ---

def aprilfools_get_counter() -> int:
return requests.get("https://api.scratch.mit.edu/surprise").json()["surprise"]


def aprilfools_increment_counter() -> int:
return requests.post("https://api.scratch.mit.edu/surprise").json()["surprise"]


# --- Resources ---
def get_resource_urls():
return requests.get("https://resources.scratch.mit.edu/localized-urls.json").json()


# --- Misc ---
# I'm not sure what to label this as
def scratch_team_members() -> dict:
Expand All @@ -115,3 +146,66 @@ def scratch_team_members() -> dict:
text = text.split("\"}]')")[0] + "\"}]"

return json.loads(text)


def translate(language: str | Languages, text: str = "hello"):
if language.lower() not in Languages.all_of("code", str.lower):
if language.lower() in Languages.all_of("name", str.lower):
language = Languages.find(language.lower(), apply_func=str.lower).code

lang = Languages.find(language, "code", str.lower)
if lang is None:
raise ValueError(f"{language} is not a supported translate language")

response_json = requests.get(
f"https://translate-service.scratch.mit.edu/translate?language={lang.code}&text={text}").json()

if "result" in response_json:
return response_json["result"]
else:
raise BadRequest(f"Language '{language}' does not seem to be valid.\nResponse: {response_json}")


def text2speech(text: str = "hello", gender: str = "female", language: str = "en-US"):
"""
Sends a request to Scratch's TTS synthesis service.
Returns:
- The TTS audio (mp3) as bytes
- The playback rate (e.g. for giant it would be 0.84)
"""
if gender == "female" or gender == "alto":
gender = ("female", 1)
elif gender == "male" or gender == "tenor":
gender = ("male", 1)
elif gender == "squeak":
gender = ("female", 1.19)
elif gender == "giant":
gender = ("male", .84)
elif gender == "kitten":
gender = ("female", 1.41)
split = text.split(' ')
text = ''
for token in split:
if token.strip() != '':
text += "meow "
else:
gender = ("female", 1)

og_lang = language
if isinstance(language, Languages):
language = language.value.tts_locale

if language is None:
raise ValueError(f"Language '{og_lang}' is not a supported tts language")

if language.lower() not in Languages.all_of("tts_locale", str.lower):
if language.lower() in Languages.all_of("name", str.lower):
language = Languages.find(language.lower(), apply_func=str.lower).tts_locale

lang = Languages.find(language, "tts_locale")
if lang is None or language is None:
raise ValueError(f"Language '{og_lang}' is not a supported tts language")

response = requests.get(f"https://synthesis-service.scratch.mit.edu/synth"
f"?locale={lang.tts_locale}&gender={gender[0]}&text={text}")
return response.content, gender[1]
138 changes: 138 additions & 0 deletions scratchattach/utils/supportedlangs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""
List of supported languages of scratch's translate and text2speech extensions.
Adapted from https://translate-service.scratch.mit.edu/supported?language=en
"""

from enum import Enum
from typing import Callable


class _Language:
def __init__(self, name: str = None, code: str = None, locales: list[str] = None, tts_locale: str = None,
single_gender: bool = None):
self.name = name
self.code = code
self.locales = locales
self.tts_locale = tts_locale
self.single_gender = single_gender

def __repr__(self):
ret = "Language("
for attr in self.__dict__.keys():
if not attr.startswith("_"):
val = getattr(self, attr)
ret += f"{repr(val)}, "
if ret.endswith(", "):
ret = ret[:-2]

ret += ')'
return ret

def __str__(self):
return f"Language<{self.name} - {self.code}>"


class Languages(Enum):
Albanian = _Language('Albanian', 'sq', None, None, None)
Amharic = _Language('Amharic', 'am', None, None, None)
Arabic = _Language('Arabic', 'ar', ['ar'], 'arb', True)
Armenian = _Language('Armenian', 'hy', None, None, None)
Azerbaijani = _Language('Azerbaijani', 'az', None, None, None)
Basque = _Language('Basque', 'eu', None, None, None)
Belarusian = _Language('Belarusian', 'be', None, None, None)
Bulgarian = _Language('Bulgarian', 'bg', None, None, None)
Catalan = _Language('Catalan', 'ca', None, None, None)
Chinese_Traditional = _Language('Chinese (Traditional)', 'zh-tw', None, None, None)
Croatian = _Language('Croatian', 'hr', None, None, None)
Czech = _Language('Czech', 'cs', None, None, None)
Danish = _Language('Danish', 'da', ['da'], 'da-DK', False)
Dutch = _Language('Dutch', 'nl', ['nl'], 'nl-NL', False)
English = _Language('English', 'en', ['en'], 'en-US', False)
Esperanto = _Language('Esperanto', 'eo', None, None, None)
Estonian = _Language('Estonian', 'et', None, None, None)
Finnish = _Language('Finnish', 'fi', None, None, None)
French = _Language('French', 'fr', ['fr'], 'fr-FR', False)
Galician = _Language('Galician', 'gl', None, None, None)
German = _Language('German', 'de', ['de'], 'de-DE', False)
Greek = _Language('Greek', 'el', None, None, None)
Haitian_Creole = _Language('Haitian Creole', 'ht', None, None, None)
Hindi = _Language('Hindi', 'hi', ['hi'], 'hi-IN', True)
Hungarian = _Language('Hungarian', 'hu', None, None, None)
Icelandic = _Language('Icelandic', 'is', ['is'], 'is-IS', False)
Indonesian = _Language('Indonesian', 'id', None, None, None)
Irish = _Language('Irish', 'ga', None, None, None)
Italian = _Language('Italian', 'it', ['it'], 'it-IT', False)
Japanese = _Language('Japanese', 'ja', ['ja', 'ja-hira'], 'ja-JP', False)
Kannada = _Language('Kannada', 'kn', None, None, None)
Korean = _Language('Korean', 'ko', ['ko'], 'ko-KR', True)
Kurdish_Kurmanji = _Language('Kurdish (Kurmanji)', 'ku', None, None, None)
Latin = _Language('Latin', 'la', None, None, None)
Latvian = _Language('Latvian', 'lv', None, None, None)
Lithuanian = _Language('Lithuanian', 'lt', None, None, None)
Macedonian = _Language('Macedonian', 'mk', None, None, None)
Malay = _Language('Malay', 'ms', None, None, None)
Malayalam = _Language('Malayalam', 'ml', None, None, None)
Maltese = _Language('Maltese', 'mt', None, None, None)
Maori = _Language('Maori', 'mi', None, None, None)
Marathi = _Language('Marathi', 'mr', None, None, None)
Mongolian = _Language('Mongolian', 'mn', None, None, None)
Myanmar_Burmese = _Language('Myanmar (Burmese)', 'my', None, None, None)
Persian = _Language('Persian', 'fa', None, None, None)
Polish = _Language('Polish', 'pl', ['pl'], 'pl-PL', False)
Portuguese = _Language('Portuguese', 'pt', None, None, None)
Romanian = _Language('Romanian', 'ro', ['ro'], 'ro-RO', True)
Russian = _Language('Russian', 'ru', ['ru'], 'ru-RU', False)
Scots_Gaelic = _Language('Scots Gaelic', 'gd', None, None, None)
Serbian = _Language('Serbian', 'sr', None, None, None)
Slovak = _Language('Slovak', 'sk', None, None, None)
Slovenian = _Language('Slovenian', 'sl', None, None, None)
Spanish = _Language('Spanish', 'es', None, None, None)
Swedish = _Language('Swedish', 'sv', ['sv'], 'sv-SE', True)
Telugu = _Language('Telugu', 'te', None, None, None)
Thai = _Language('Thai', 'th', None, None, None)
Turkish = _Language('Turkish', 'tr', ['tr'], 'tr-TR', True)
Ukrainian = _Language('Ukrainian', 'uk', None, None, None)
Uzbek = _Language('Uzbek', 'uz', None, None, None)
Vietnamese = _Language('Vietnamese', 'vi', None, None, None)
Welsh = _Language('Welsh', 'cy', ['cy'], 'cy-GB', True)
Zulu = _Language('Zulu', 'zu', None, None, None)
Hebrew = _Language('Hebrew', 'he', None, None, None)
Chinese_Simplified = _Language('Chinese (Simplified)', 'zh-cn', None, None, None)
Mandarin = Chinese_Simplified

cmn_CN = _Language(None, None, ['zh-cn', 'zh-tw'], 'cmn-CN', True)
nb_NO = _Language(None, None, ['nb', 'nn'], 'nb-NO', True)
pt_BR = _Language(None, None, ['pt-br'], 'pt-BR', False)
Brazilian = pt_BR
pt_PT = _Language(None, None, ['pt'], 'pt-PT', False)
es_ES = _Language(None, None, ['es'], 'es-ES', False)
es_US = _Language(None, None, ['es-419'], 'es-US', False)

@staticmethod
def find(value, by: str = "name", apply_func: Callable = None) -> _Language:
if apply_func is None:
def apply_func(x):
return x

for lang_enum in Languages:
lang = lang_enum.value
try:
if apply_func(getattr(lang, by)) == value:
return lang
except TypeError:
pass

@staticmethod
def all_of(attr_name: str = "name", apply_func: Callable = None):
if apply_func is None:
def apply_func(x):
return x

for lang_enum in Languages:
lang = lang_enum.value
attr = getattr(lang, attr_name)
try:
yield apply_func(attr)

except TypeError:
yield attr