Re-implement lucene a bit
This commit is contained in:
parent
f4f9b0bf3c
commit
5eef5f0f22
@ -5,10 +5,7 @@ use std::{num::ParseIntError, str::FromStr};
|
|||||||
use musichoard::{
|
use musichoard::{
|
||||||
collection::{album::AlbumDate, musicbrainz::Mbid},
|
collection::{album::AlbumDate, musicbrainz::Mbid},
|
||||||
external::musicbrainz::{
|
external::musicbrainz::{
|
||||||
api::{
|
api::{search::SearchReleaseGroupRequest, MusicBrainzClient},
|
||||||
search::{Expression, Query, SearchReleaseGroup},
|
|
||||||
MusicBrainzClient,
|
|
||||||
},
|
|
||||||
http::MusicBrainzHttp,
|
http::MusicBrainzHttp,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -93,18 +90,21 @@ fn main() {
|
|||||||
let title: String;
|
let title: String;
|
||||||
let rgid: Mbid;
|
let rgid: Mbid;
|
||||||
|
|
||||||
let query: Query<SearchReleaseGroup> = match opt.command {
|
let query = match opt.command {
|
||||||
OptCommand::Title(opt_title) => {
|
OptCommand::Title(opt_title) => {
|
||||||
arid = opt_title.arid.into();
|
arid = opt_title.arid.into();
|
||||||
date = opt_title.date.map(Into::into).unwrap_or_default();
|
date = opt_title.date.map(Into::into).unwrap_or_default();
|
||||||
title = opt_title.title;
|
title = opt_title.title;
|
||||||
Query::expression(Expression::arid(&arid))
|
SearchReleaseGroupRequest::new()
|
||||||
.and(Expression::release_group(&title))
|
.arid(&arid)
|
||||||
.and(Expression::first_release_date(&date))
|
.and()
|
||||||
|
.release_group(&title)
|
||||||
|
.and()
|
||||||
|
.first_release_date(&date)
|
||||||
}
|
}
|
||||||
OptCommand::Rgid(opt_rgid) => {
|
OptCommand::Rgid(opt_rgid) => {
|
||||||
rgid = opt_rgid.rgid.into();
|
rgid = opt_rgid.rgid.into();
|
||||||
Query::expression(Expression::rgid(&rgid))
|
SearchReleaseGroupRequest::new().rgid(&rgid)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
59
src/external/musicbrainz/api/search/mod.rs
vendored
59
src/external/musicbrainz/api/search/mod.rs
vendored
@ -3,7 +3,7 @@ mod query;
|
|||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
pub use query::{Expression, Query};
|
use query::{impl_term, EmptyQuery, EmptyQueryJoin, QueryJoin};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
|
|
||||||
@ -12,8 +12,8 @@ use crate::{
|
|||||||
core::collection::album::{AlbumPrimaryType, AlbumSecondaryType},
|
core::collection::album::{AlbumPrimaryType, AlbumSecondaryType},
|
||||||
external::musicbrainz::{
|
external::musicbrainz::{
|
||||||
api::{
|
api::{
|
||||||
ApiDisplay, Error, MusicBrainzClient, SerdeAlbumDate, SerdeAlbumPrimaryType,
|
search::query::Query, ApiDisplay, Error, MusicBrainzClient, SerdeAlbumDate,
|
||||||
SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL,
|
SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid, MB_BASE_URL,
|
||||||
},
|
},
|
||||||
IMusicBrainzHttp,
|
IMusicBrainzHttp,
|
||||||
},
|
},
|
||||||
@ -41,28 +41,6 @@ pub enum SearchReleaseGroup<'a> {
|
|||||||
Rgid(&'a Mbid),
|
Rgid(&'a Mbid),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Expression<SearchReleaseGroup<'a>> {
|
|
||||||
pub fn no_field(string: &'a str) -> Self {
|
|
||||||
Expression::Term(SearchReleaseGroup::NoField(string))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn arid(arid: &'a Mbid) -> Self {
|
|
||||||
Expression::Term(SearchReleaseGroup::Arid(arid))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn first_release_date(first_release_date: &'a AlbumDate) -> Self {
|
|
||||||
Expression::Term(SearchReleaseGroup::FirstReleaseDate(first_release_date))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn release_group(release_group: &'a str) -> Self {
|
|
||||||
Expression::Term(SearchReleaseGroup::ReleaseGroup(release_group))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rgid(rgid: &'a Mbid) -> Self {
|
|
||||||
Expression::Term(SearchReleaseGroup::Rgid(rgid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> fmt::Display for SearchReleaseGroup<'a> {
|
impl<'a> fmt::Display for SearchReleaseGroup<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@ -81,6 +59,17 @@ impl<'a> fmt::Display for SearchReleaseGroup<'a> {
|
|||||||
|
|
||||||
pub type SearchReleaseGroupRequest<'a> = Query<SearchReleaseGroup<'a>>;
|
pub type SearchReleaseGroupRequest<'a> = Query<SearchReleaseGroup<'a>>;
|
||||||
|
|
||||||
|
impl_term!(no_field, SearchReleaseGroup<'a>, NoField, &'a str);
|
||||||
|
impl_term!(arid, SearchReleaseGroup<'a>, Arid, &'a Mbid);
|
||||||
|
impl_term!(
|
||||||
|
first_release_date,
|
||||||
|
SearchReleaseGroup<'a>,
|
||||||
|
FirstReleaseDate,
|
||||||
|
&'a AlbumDate
|
||||||
|
);
|
||||||
|
impl_term!(release_group, SearchReleaseGroup<'a>, ReleaseGroup, &'a str);
|
||||||
|
impl_term!(rgid, SearchReleaseGroup<'a>, Rgid, &'a Mbid);
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct SearchReleaseGroupResponse {
|
pub struct SearchReleaseGroupResponse {
|
||||||
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
|
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
|
||||||
@ -212,16 +201,19 @@ mod tests {
|
|||||||
let title: AlbumId = AlbumId::new("an album");
|
let title: AlbumId = AlbumId::new("an album");
|
||||||
let date = (1986, 4).into();
|
let date = (1986, 4).into();
|
||||||
|
|
||||||
let query = Query::expression(Expression::arid(&arid))
|
let query = SearchReleaseGroupRequest::new()
|
||||||
.and(Expression::release_group(&title.title))
|
.arid(&arid)
|
||||||
.and(Expression::first_release_date(&date));
|
.and()
|
||||||
|
.release_group(&title.title)
|
||||||
|
.and()
|
||||||
|
.first_release_date(&date);
|
||||||
|
|
||||||
let matches = client.search_release_group(query).unwrap();
|
let matches = client.search_release_group(query).unwrap();
|
||||||
assert_eq!(matches, response);
|
assert_eq!(matches, response);
|
||||||
|
|
||||||
let rgid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
|
let rgid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
|
||||||
|
|
||||||
let query = Query::expression(Expression::rgid(&rgid));
|
let query = SearchReleaseGroupRequest::new().rgid(&rgid);
|
||||||
|
|
||||||
let matches = client.search_release_group(query).unwrap();
|
let matches = client.search_release_group(query).unwrap();
|
||||||
assert_eq!(matches, response);
|
assert_eq!(matches, response);
|
||||||
@ -253,9 +245,12 @@ mod tests {
|
|||||||
let title: AlbumId = AlbumId::new("an album");
|
let title: AlbumId = AlbumId::new("an album");
|
||||||
let date = AlbumDate::default();
|
let date = AlbumDate::default();
|
||||||
|
|
||||||
let query = Query::expression(Expression::arid(&arid))
|
let query = SearchReleaseGroupRequest::new()
|
||||||
.and(Expression::release_group(&title.title))
|
.arid(&arid)
|
||||||
.and(Expression::first_release_date(&date));
|
.and()
|
||||||
|
.release_group(&title.title)
|
||||||
|
.and()
|
||||||
|
.first_release_date(&date);
|
||||||
|
|
||||||
let _ = client.search_release_group(query).unwrap();
|
let _ = client.search_release_group(query).unwrap();
|
||||||
}
|
}
|
||||||
|
212
src/external/musicbrainz/api/search/query.rs
vendored
212
src/external/musicbrainz/api/search/query.rs
vendored
@ -1,4 +1,4 @@
|
|||||||
use std::fmt;
|
use std::{fmt, marker::PhantomData};
|
||||||
|
|
||||||
pub enum Logical {
|
pub enum Logical {
|
||||||
Unary(Unary),
|
Unary(Unary),
|
||||||
@ -49,6 +49,18 @@ pub enum Expression<Entity> {
|
|||||||
Expr(Query<Entity>),
|
Expr(Query<Entity>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<Entity> From<Entity> for Expression<Entity> {
|
||||||
|
fn from(value: Entity) -> Self {
|
||||||
|
Expression::Term(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Entity> From<Query<Entity>> for Expression<Entity> {
|
||||||
|
fn from(value: Query<Entity>) -> Self {
|
||||||
|
Expression::Expr(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<Entity: fmt::Display> fmt::Display for Expression<Entity> {
|
impl<Entity: fmt::Display> fmt::Display for Expression<Entity> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@ -58,6 +70,55 @@ impl<Entity: fmt::Display> fmt::Display for Expression<Entity> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct EmptyQuery<Entity> {
|
||||||
|
_marker: PhantomData<Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Entity> Default for EmptyQuery<Entity> {
|
||||||
|
fn default() -> Self {
|
||||||
|
EmptyQuery {
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Entity> EmptyQuery<Entity> {
|
||||||
|
pub fn expression<Expr: Into<Expression<Entity>>>(self, expr: Expr) -> Query<Entity> {
|
||||||
|
Query {
|
||||||
|
left: (None, Box::new(expr.into())),
|
||||||
|
right: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn require(self) -> EmptyQueryJoin<Entity> {
|
||||||
|
EmptyQueryJoin {
|
||||||
|
unary: Unary::Require,
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prohibit(self) -> EmptyQueryJoin<Entity> {
|
||||||
|
EmptyQueryJoin {
|
||||||
|
unary: Unary::Prohibit,
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EmptyQueryJoin<Entity> {
|
||||||
|
unary: Unary,
|
||||||
|
_marker: PhantomData<Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Entity> EmptyQueryJoin<Entity> {
|
||||||
|
pub fn expression<Expr: Into<Expression<Entity>>>(self, expr: Expr) -> Query<Entity> {
|
||||||
|
Query {
|
||||||
|
left: (Some(self.unary), Box::new(expr.into())),
|
||||||
|
right: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Query<Entity> {
|
pub struct Query<Entity> {
|
||||||
left: (Option<Unary>, Box<Expression<Entity>>),
|
left: (Option<Unary>, Box<Expression<Entity>>),
|
||||||
right: Vec<(Logical, Box<Expression<Entity>>)>,
|
right: Vec<(Logical, Box<Expression<Entity>>)>,
|
||||||
@ -79,94 +140,135 @@ impl<Entity: fmt::Display> fmt::Display for Query<Entity> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Entity> Query<Entity> {
|
impl<Entity> Query<Entity> {
|
||||||
pub fn expression(expr: Expression<Entity>) -> Self {
|
pub fn new() -> EmptyQuery<Entity> {
|
||||||
Query {
|
EmptyQuery::default()
|
||||||
left: (None, Box::new(expr)),
|
}
|
||||||
right: vec![],
|
|
||||||
|
pub fn require(self) -> QueryJoin<Entity> {
|
||||||
|
QueryJoin {
|
||||||
|
logical: Logical::Unary(Unary::Require),
|
||||||
|
query: self,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn require(expr: Expression<Entity>) -> Self {
|
pub fn prohibit(self) -> QueryJoin<Entity> {
|
||||||
Query {
|
QueryJoin {
|
||||||
left: (Some(Unary::Require), Box::new(expr)),
|
logical: Logical::Unary(Unary::Prohibit),
|
||||||
right: vec![],
|
query: self,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn and_require(mut self, expr: Expression<Entity>) -> Self {
|
pub fn and(self) -> QueryJoin<Entity> {
|
||||||
self.right
|
QueryJoin {
|
||||||
.push((Logical::Unary(Unary::Require), Box::new(expr)));
|
logical: Logical::Binary(Boolean::And),
|
||||||
self
|
query: self,
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prohibit(expr: Expression<Entity>) -> Self {
|
|
||||||
Query {
|
|
||||||
left: (Some(Unary::Prohibit), Box::new(expr)),
|
|
||||||
right: vec![],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn and_prohibit(mut self, expr: Expression<Entity>) -> Self {
|
pub fn or(self) -> QueryJoin<Entity> {
|
||||||
self.right
|
QueryJoin {
|
||||||
.push((Logical::Unary(Unary::Prohibit), Box::new(expr)));
|
logical: Logical::Binary(Boolean::Or),
|
||||||
self
|
query: self,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn and(mut self, expr: Expression<Entity>) -> Self {
|
pub fn not(self) -> QueryJoin<Entity> {
|
||||||
self.right
|
QueryJoin {
|
||||||
.push((Logical::Binary(Boolean::And), Box::new(expr)));
|
logical: Logical::Binary(Boolean::Not),
|
||||||
self
|
query: self,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn or(mut self, expr: Expression<Entity>) -> Self {
|
|
||||||
self.right
|
|
||||||
.push((Logical::Binary(Boolean::Or), Box::new(expr)));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn not(mut self, expr: Expression<Entity>) -> Self {
|
|
||||||
self.right
|
|
||||||
.push((Logical::Binary(Boolean::Not), Box::new(expr)));
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct QueryJoin<Entity> {
|
||||||
|
logical: Logical,
|
||||||
|
query: Query<Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Entity> QueryJoin<Entity> {
|
||||||
|
pub fn expression<Expr: Into<Expression<Entity>>>(mut self, expr: Expr) -> Query<Entity> {
|
||||||
|
self.query.right.push((self.logical, Box::new(expr.into())));
|
||||||
|
self.query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_term {
|
||||||
|
($name:ident, $enum:ty, $variant:ident, $type:ty) => {
|
||||||
|
impl<'a> EmptyQuery<$enum> {
|
||||||
|
pub fn $name(self, $name: $type) -> Query<$enum> {
|
||||||
|
self.expression(<$enum>::$variant($name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EmptyQueryJoin<$enum> {
|
||||||
|
pub fn $name(self, $name: $type) -> Query<$enum> {
|
||||||
|
self.expression(<$enum>::$variant($name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> QueryJoin<$enum> {
|
||||||
|
pub fn $name(self, $name: $type) -> Query<$enum> {
|
||||||
|
self.expression(<$enum>::$variant($name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use impl_term;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use crate::external::musicbrainz::api::search::SearchReleaseGroupRequest;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lucene_logical() {
|
fn lucene_logical() {
|
||||||
let query = Query::expression(Expression::no_field("jakarta apache"))
|
let query = SearchReleaseGroupRequest::new()
|
||||||
.or(Expression::no_field("jakarta"));
|
.no_field("jakarta apache")
|
||||||
|
.or()
|
||||||
|
.no_field("jakarta");
|
||||||
assert_eq!(format!("{query}"), "\"jakarta apache\" OR \"jakarta\"");
|
assert_eq!(format!("{query}"), "\"jakarta apache\" OR \"jakarta\"");
|
||||||
|
|
||||||
let query = Query::expression(Expression::no_field("jakarta apache"))
|
let query = SearchReleaseGroupRequest::new()
|
||||||
.and(Expression::no_field("jakarta"));
|
.no_field("jakarta apache")
|
||||||
|
.and()
|
||||||
|
.no_field("jakarta");
|
||||||
assert_eq!(format!("{query}"), "\"jakarta apache\" AND \"jakarta\"");
|
assert_eq!(format!("{query}"), "\"jakarta apache\" AND \"jakarta\"");
|
||||||
|
|
||||||
let query =
|
let query = SearchReleaseGroupRequest::new()
|
||||||
Query::require(Expression::no_field("jakarta")).or(Expression::no_field("lucene"));
|
.require()
|
||||||
|
.no_field("jakarta")
|
||||||
|
.or()
|
||||||
|
.no_field("lucene");
|
||||||
assert_eq!(format!("{query}"), "+\"jakarta\" OR \"lucene\"");
|
assert_eq!(format!("{query}"), "+\"jakarta\" OR \"lucene\"");
|
||||||
|
|
||||||
let query = Query::expression(Expression::no_field("jakarta apache"))
|
let query = SearchReleaseGroupRequest::new()
|
||||||
.not(Expression::no_field("Apache Lucene"));
|
.no_field("jakarta apache")
|
||||||
|
.not()
|
||||||
|
.no_field("Apache Lucene");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{query}"),
|
format!("{query}"),
|
||||||
"\"jakarta apache\" NOT \"Apache Lucene\""
|
"\"jakarta apache\" NOT \"Apache Lucene\""
|
||||||
);
|
);
|
||||||
|
|
||||||
let query = Query::expression(Expression::no_field("jakarta apache"))
|
let query = SearchReleaseGroupRequest::new()
|
||||||
.and_prohibit(Expression::no_field("Apache Lucene"));
|
.no_field("jakarta apache")
|
||||||
|
.prohibit()
|
||||||
|
.no_field("Apache Lucene");
|
||||||
assert_eq!(format!("{query}"), "\"jakarta apache\" -\"Apache Lucene\"");
|
assert_eq!(format!("{query}"), "\"jakarta apache\" -\"Apache Lucene\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lucene_grouping() {
|
fn lucene_grouping() {
|
||||||
let query = Query::expression(Expression::Expr(
|
let query = SearchReleaseGroupRequest::new()
|
||||||
Query::expression(Expression::no_field("jakarta")).or(Expression::no_field("apache")),
|
.expression(
|
||||||
))
|
SearchReleaseGroupRequest::new()
|
||||||
.and(Expression::no_field("website"));
|
.no_field("jakarta")
|
||||||
|
.or()
|
||||||
|
.no_field("apache"),
|
||||||
|
)
|
||||||
|
.and()
|
||||||
|
.no_field("website");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{query}"),
|
format!("{query}"),
|
||||||
"(\"jakarta\" OR \"apache\") AND \"website\""
|
"(\"jakarta\" OR \"apache\") AND \"website\""
|
||||||
|
11
src/tui/lib/external/musicbrainz/mod.rs
vendored
11
src/tui/lib/external/musicbrainz/mod.rs
vendored
@ -7,7 +7,7 @@ use musichoard::{
|
|||||||
},
|
},
|
||||||
external::musicbrainz::{
|
external::musicbrainz::{
|
||||||
api::{
|
api::{
|
||||||
search::{Expression, Query, SearchReleaseGroupResponseReleaseGroup},
|
search::{SearchReleaseGroupRequest, SearchReleaseGroupResponseReleaseGroup},
|
||||||
MusicBrainzClient,
|
MusicBrainzClient,
|
||||||
},
|
},
|
||||||
IMusicBrainzHttp,
|
IMusicBrainzHttp,
|
||||||
@ -37,9 +37,12 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
|
|||||||
// with just the year should be enough anyway.
|
// with just the year should be enough anyway.
|
||||||
let date = AlbumDate::new(album.date.year, None, None);
|
let date = AlbumDate::new(album.date.year, None, None);
|
||||||
|
|
||||||
let query = Query::expression(Expression::arid(arid))
|
let query = SearchReleaseGroupRequest::new()
|
||||||
.and(Expression::first_release_date(&date))
|
.arid(arid)
|
||||||
.and(Expression::release_group(&album.id.title));
|
.and()
|
||||||
|
.first_release_date(&date)
|
||||||
|
.and()
|
||||||
|
.release_group(&album.id.title);
|
||||||
|
|
||||||
let mb_response = self.client.search_release_group(query)?;
|
let mb_response = self.client.search_release_group(query)?;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user