diff --git a/nise-frontend/src/app/api-python/api-python.component.css b/nise-frontend/src/app/api-python/api-python.component.css new file mode 100644 index 0000000..e69de29 diff --git a/nise-frontend/src/app/api-python/api-python.component.html b/nise-frontend/src/app/api-python/api-python.component.html new file mode 100644 index 0000000..7b96e55 --- /dev/null +++ b/nise-frontend/src/app/api-python/api-python.component.html @@ -0,0 +1,20 @@ +
+
+ +

# super duper official python library

+

if you're using python, you can download/copy the api wrapper into your project and get (more or less) type-safe methods.

+

last update: v20240612

+

remember to pip install requests

+
+ + +
+ + +
+

Example usage

+
{{ pythonUsage }}
+
+
+ + diff --git a/nise-frontend/src/app/api-python/api-python.component.ts b/nise-frontend/src/app/api-python/api-python.component.ts new file mode 100644 index 0000000..33b4e80 --- /dev/null +++ b/nise-frontend/src/app/api-python/api-python.component.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; +import {Observable} from "rxjs"; +import {HttpClient} from "@angular/common/http"; +import {RouterLink} from "@angular/router"; + +@Component({ + selector: 'app-api-python', + standalone: true, + imports: [ + RouterLink + ], + templateUrl: './api-python.component.html', + styleUrl: './api-python.component.css' +}) +export class ApiPythonComponent { + + pythonScript!: string; + pythonUsage!: string; + + ngOnInit(): void { + this.getPythonScript().subscribe( + data => this.pythonScript = data, + error => console.error(error) + ); + + this.getPythonScriptUsage().subscribe( + data => this.pythonUsage = data, + error => console.error(error) + ); + } + + constructor(private http: HttpClient) { } + + getPythonScript(): Observable { + return this.http.get('/assets/nise.py', { responseType: 'text' }); + } + + getPythonScriptUsage(): Observable { + return this.http.get('/assets/nise_usage.py', { responseType: 'text' }); + } + + + copyToClipboard(): void { + navigator.clipboard.writeText(this.pythonScript) + .then(() => alert('Copied to clipboard!')) + .catch(() => alert('Failed to copy to clipboard!')); + } + + download(): void { + const blob = new Blob([this.pythonScript], {type: 'text/plain'}); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'nise.py'; + a.click(); + window.URL.revokeObjectURL(url); + } + +} diff --git a/nise-frontend/src/app/api/api.component.css b/nise-frontend/src/app/api/api.component.css index 6d94c53..ffcda7f 100644 --- a/nise-frontend/src/app/api/api.component.css +++ b/nise-frontend/src/app/api/api.component.css @@ -2,3 +2,11 @@ section { padding-bottom: 30px; border-bottom: 2px dotted rgba(224, 224, 224, 0.32); } + +.cursor-pointer { + cursor: pointer; +} + +.py-wrapper:hover { + background-color: rgba(139, 139, 139, 0.32); +} diff --git a/nise-frontend/src/app/api/api.component.html b/nise-frontend/src/app/api/api.component.html index c915e88..9285664 100644 --- a/nise-frontend/src/app/api/api.component.html +++ b/nise-frontend/src/app/api/api.component.html @@ -1,9 +1,17 @@

# /api/ stuff

+
+

+ NEW! check out the python api + wrapper +

+
+

if you'd like to retrieve data from our database, you are invited to use the same endpoints meant for the frontend but in a programmatic way. currently there's no rate limits.

+

if you have any issues related to CloudFlare, let me know on discord. currently all api requests are routed via CF so it's possible it might block certain IPs. diff --git a/nise-frontend/src/app/app-routing.module.ts b/nise-frontend/src/app/app-routing.module.ts index 6b8a4ce..474969e 100644 --- a/nise-frontend/src/app/app-routing.module.ts +++ b/nise-frontend/src/app/app-routing.module.ts @@ -11,6 +11,7 @@ import {ContributeComponent} from "./contribute/contribute.component"; import {BanlistComponent} from "./banlist/banlist.component"; import {ProfileComponent} from "./profile/profile.component"; import {ApiComponent} from "./api/api.component"; +import {ApiPythonComponent} from "./api-python/api-python.component"; const routes: Routes = [ {path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'}, @@ -33,6 +34,7 @@ const routes: Routes = [ {path: 'contribute', component: ContributeComponent, title: '/contribute/ <3'}, {path: 'docs', component: ApiComponent, title: '/api/ explanation'}, + {path: 'docs/py', component: ApiPythonComponent, title: 'python lib'}, {path: '**', component: HomeComponent, title: '/nise.moe/'}, ]; diff --git a/nise-frontend/src/assets/nise.py b/nise-frontend/src/assets/nise.py new file mode 100644 index 0000000..b6136f0 --- /dev/null +++ b/nise-frontend/src/assets/nise.py @@ -0,0 +1,328 @@ +import datetime + +import requests +import json +from dataclasses import dataclass, field +from typing import List, Dict, Optional, Any + + +@dataclass +class SearchPagination: + currentPage: int + pageSize: int + totalPages: int + totalResults: int + + +@dataclass +class ScoreSearchResult: + user_id: int + user_username: str + user_join_date: str + user_country: str + user_rank: int + user_pp_raw: float + user_accuracy: float + user_playcount: int + user_total_score: int + user_ranked_score: int + user_seconds_played: int + user_count_300: int + user_count_100: int + user_count_50: int + user_count_miss: int + user_is_banned: bool + id: int + beatmap_id: int + count_300: int + count_100: int + count_50: int + count_miss: int + date: str + max_combo: int + mods: int + perfect: bool + pp: float + rank: str + replay_id: int + score: int + ur: float + frametime: float + edge_hits: int + snaps: int + adjusted_ur: float + mean_error: float + error_variance: float + error_standard_deviation: float + minimum_error: int + maximum_error: int + error_range: int + error_coefficient_of_variation: float + error_kurtosis: float + error_skewness: float + keypresses_median_adjusted: float + keypresses_standard_deviation_adjusted: float + sliderend_release_median_adjusted: float + sliderend_release_standard_deviation_adjusted: float + beatmap_artist: str + beatmap_beatmapset_id: int + beatmap_creator: str + beatmap_source: Optional[str] + beatmap_star_rating: float + beatmap_title: str + beatmap_version: str + + +@dataclass +class ScoreSearchResult: + pagination: SearchPagination + results: List[ScoreSearchResult] + + +@dataclass +class UserSearchResult: + user_id: int + user_username: str + user_join_date: str + user_country: str + user_rank: int + user_pp_raw: float + user_accuracy: float + user_playcount: int + user_total_score: int + user_ranked_score: int + user_seconds_played: int + user_count_300: int + user_count_100: int + user_count_50: int + user_count_miss: int + user_is_banned: bool + + +@dataclass +class UserSearchResult: + pagination: SearchPagination + results: List[UserSearchResult] + + +@dataclass +class ErrorDistribution: + countMiss: float + count300: float + count100: float + count50: float + + +@dataclass +class ReplayData: + replay_id: int + user_id: int + username: str + date: str + beatmap_id: int + beatmap_beatmapset_id: int + beatmap_artist: str + beatmap_title: str + beatmap_star_rating: float + beatmap_creator: str + beatmap_version: str + beatmap_max_combo: int + beatmap_total_length: int + beatmap_bpm: float + beatmap_accuracy: float + beatmap_ar: float + beatmap_cs: float + beatmap_drain: float + beatmap_count_circles: int + beatmap_count_sliders: int + beatmap_count_spinners: int + score: int + mods: List[str] + rank: str + ur: float + adjusted_ur: float + frametime: int + snaps: int + hits: int + mean_error: float + error_variance: float + error_standard_deviation: float + minimum_error: float + maximum_error: float + error_range: float + error_coefficient_of_variation: float + error_kurtosis: float + error_skewness: float + comparable_samples: int + comparable_mean_error: float + comparable_error_variance: float + comparable_error_standard_deviation: float + comparable_minimum_error: float + comparable_maximum_error: float + comparable_error_range: float + comparable_error_coefficient_of_variation: float + comparable_error_kurtosis: float + comparable_error_skewness: float + comparable_adjusted_ur: float + pp: float + perfect: bool + max_combo: int + count_300: int + count_100: int + count_50: int + count_miss: int + error_distribution: Dict[str, ErrorDistribution] + similar_scores: List[Any] + charts: Any + + +@dataclass +class UserDetails: + user_id: int + username: str + rank: int + pp_raw: float + join_date: str + seconds_played: int + country: str + country_rank: Optional[int] + playcount: int + suspicious_scores: int + stolen_replays: int + + +@dataclass +class QueueDetails: + isProcessing: bool + lastCompletedUpdate: Optional[str] + canUpdate: bool + progressCurrent: Optional[int] + progressTotal: Optional[int] + + +@dataclass +class UserProfile: + user_details: UserDetails + queue_details: QueueDetails + suspicious_scores: List[dict] = field(default_factory=list) + similar_replays: List[dict] = field(default_factory=list) + total_scores: int = 0 + is_banned: bool = False + approximate_ban_date: Optional[str] = None + + +@dataclass +class SuspiciousScoreEntry: + user_id: int + username: str + replay_id: int + date: str + beatmap_id: int + beatmap_beatmapset_id: int + beatmap_title: str + beatmap_star_rating: float + pp: float + frametime: float + ur: float + + +@dataclass +class StolenReplayEntry: + replay_id_1: int + replay_id_2: int + user_id_1: int + user_id_2: int + username_1: str + username_2: str + beatmap_beatmapset_id: int + replay_date_1: str + replay_date_2: str + replay_pp_1: float + replay_pp_2: float + beatmap_id: int + beatmap_title: str + beatmap_star_rating: float + similarity: float + + +@dataclass +class BanListPagination: + currentPage: int + pageSize: int + totalPages: int + totalResults: int + + +@dataclass +class BanListEntry: + userId: int + username: str + secondsPlayed: int + pp: float + rank: int + isBanned: bool + approximateBanTime: datetime + lastUpdate: datetime + + +@dataclass +class BanList: + pagination: BanListPagination + users: List[BanListEntry] + + +class NiseAPI: + def __init__(self, base_url="https://nise.moe"): + self.base_url = base_url + self.headers = { + "X-NISE-API": "20240218", + "Accept": "application/json", + "Content-Type": "application/json" + } + + def search_scores(self, query) -> ScoreSearchResult: + url = f"{self.base_url}/api/search" + response = requests.post(url, headers=self.headers, data=json.dumps(query)) + return ScoreSearchResult(**response.json()) + + def search_users(self, query) -> UserSearchResult: + url = f"{self.base_url}/api/search-user" + response = requests.post(url, headers=self.headers, data=json.dumps(query)) + return UserSearchResult(**response.json()) + + def get_single_score(self, replay_id) -> ReplayData: + url = f"{self.base_url}/api/score/{replay_id}" + response = requests.get(url, headers=self.headers) + try: + return ReplayData(**response.json()) + except (TypeError, ValueError): + raise ValueError() + + def get_user_details(self, user_id=None, username=None) -> UserProfile: + url = f"{self.base_url}/api/user-details" + data = {} + if user_id: + data["userId"] = user_id + elif username: + data["username"] = username + response = requests.post(url, headers=self.headers, data=json.dumps(data)) + try: + return UserProfile(**response.json()) + except (TypeError, ValueError): + raise ValueError() + + def get_suspicious_scores(self) -> List[SuspiciousScoreEntry]: + url = f"{self.base_url}/api/suspicious-scores" + response = requests.get(url, headers=self.headers) + return [SuspiciousScoreEntry(**item) for item in response.json()] + + def get_similar_replays(self) -> List[StolenReplayEntry]: + url = f"{self.base_url}/api/similar-replays" + response = requests.get(url, headers=self.headers) + return [StolenReplayEntry(**item) for item in response.json()] + + def get_banlist(self, page) -> BanList: + url = f"{self.base_url}/api/banlist" + data = {"page": page} + response = requests.post(url, headers=self.headers, data=json.dumps(data)) + return BanList(**response.json()) diff --git a/nise-frontend/src/assets/nise_usage.py b/nise-frontend/src/assets/nise_usage.py new file mode 100644 index 0000000..1c14129 --- /dev/null +++ b/nise-frontend/src/assets/nise_usage.py @@ -0,0 +1,94 @@ +nise_api = NiseAPI() + +scores = nise_api.get_single_score(replay_id=3808640439) +print(scores.adjusted_ur) + +user = nise_api.get_user_details(username="degenerate") +print(user) + +suspicious_scores = nise_api.get_suspicious_scores() +print(suspicious_scores) + +stolen_replays = nise_api.get_similar_replays() +print(stolen_replays) + +banlist = nise_api.get_banlist(page=1) +print(banlist) + +score_query = { + "queries": [ + { + "predicates": [ + { + "field": { + "name": "user_rank", + "type": "number" + }, + "operator": { + "operatorType": "<", + "acceptsValues": "any" + }, + "value": "50" + }, + { + "field": { + "name": "ur", + "type": "number" + }, + "operator": { + "operatorType": "<", + "acceptsValues": "any" + }, + "value": "120" + } + ], + "logicalOperator": "AND" + } + ], + "sorting": { + "field": "user_id", + "order": "ASC" + }, + "page": 1 +} +score_search = nise_api.search_scores(query=score_query) +print(score_search) + +user_query = { + "queries": [ + { + "predicates": [ + { + "field": { + "name": "rank", + "type": "number" + }, + "operator": { + "operatorType": ">", + "acceptsValues": "any" + }, + "value": "10" + }, + { + "field": { + "name": "username", + "type": "string" + }, + "operator": { + "operatorType": "=", + "acceptsValues": "any" + }, + "value": "degenerate" + } + ], + "logicalOperator": "AND" + } + ], + "sorting": { + "field": "user_id", + "order": "ASC" + }, + "page": 1 +} +user_search = nise_api.search_users(query=user_query) +print(user_search)