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
109 changes: 106 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,75 @@ def scratch_team_members() -> dict:
text = text.split("\"}]')")[0] + "\"}]"

return json.loads(text)


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

elif language.lower() in Languages.all_of("name", str.lower):
lang = Languages.find(language.lower(), "name", str.lower)

elif isinstance(language, Languages):
lang = language.value
else:
# The code will work so long as the language has a 'code' attribute, however, this is bad practice.
warnings.warn(f"{language} is not {str} or {Languages}, but {type(language)}.")

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]
142 changes: 142 additions & 0 deletions scratchattach/utils/supportedlangs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""
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 dataclasses import dataclass

from typing import Callable


@dataclass(init=True, repr=True)
class _Language:
name: str = None
code: str = None
locales: list[str] = None
tts_locale: str = None
single_gender: bool = None


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:
"""
Finds the language with the given attribute that is equal to the given value.
the apply_func will be applied to the attribute of each language object before comparison.

i.e. Languages.find("ukranian", "name", str.lower) will return the Ukrainian language dataclass object
(even though Ukrainian was spelt lowercase, since str.lower will convert the "Ukrainian" string to lowercase)
"""
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):
"""
Returns the list of each listed language's specified attribute by "attr_name"

i.e. Languages.all_of("name") will return a list of names:
["Albanian", "Amharic", ...]

The apply_func function will be applied to every list item,
i.e. Languages.all_of("name", str.lower) will return the same except in lowercase:
["albanian", "amharic", ...]
"""
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