Added arn to the main repository
This commit is contained in:
66
arn/search/AMVs.go
Normal file
66
arn/search/AMVs.go
Normal file
@ -0,0 +1,66 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
"github.com/animenotifier/notify.moe/arn/stringutils"
|
||||
)
|
||||
|
||||
// AMVs searches all anime music videos.
|
||||
func AMVs(originalTerm string, maxLength int) []*arn.AMV {
|
||||
term := strings.ToLower(stringutils.RemoveSpecialCharacters(originalTerm))
|
||||
results := make([]*Result, 0, maxLength)
|
||||
|
||||
for amv := range arn.StreamAMVs() {
|
||||
if amv.ID == originalTerm {
|
||||
return []*arn.AMV{amv}
|
||||
}
|
||||
|
||||
if amv.IsDraft {
|
||||
continue
|
||||
}
|
||||
|
||||
text := strings.ToLower(amv.Title.Canonical)
|
||||
similarity := stringutils.AdvancedStringSimilarity(term, text)
|
||||
|
||||
if similarity >= MinimumStringSimilarity {
|
||||
results = append(results, &Result{
|
||||
obj: amv,
|
||||
similarity: similarity,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
text = strings.ToLower(amv.Title.Native)
|
||||
similarity = stringutils.AdvancedStringSimilarity(term, text)
|
||||
|
||||
if similarity >= MinimumStringSimilarity {
|
||||
results = append(results, &Result{
|
||||
obj: amv,
|
||||
similarity: similarity,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Sort
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].similarity > results[j].similarity
|
||||
})
|
||||
|
||||
// Limit
|
||||
if len(results) >= maxLength {
|
||||
results = results[:maxLength]
|
||||
}
|
||||
|
||||
// Final list
|
||||
final := make([]*arn.AMV, len(results))
|
||||
|
||||
for i, result := range results {
|
||||
final[i] = result.obj.(*arn.AMV)
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
56
arn/search/All.go
Normal file
56
arn/search/All.go
Normal file
@ -0,0 +1,56 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"github.com/aerogo/flow"
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
)
|
||||
|
||||
// MinimumStringSimilarity is the minimum JaroWinkler distance we accept for search results.
|
||||
const MinimumStringSimilarity = 0.89
|
||||
|
||||
// popularityDamping reduces the factor of popularity in search results.
|
||||
const popularityDamping = 0.0009
|
||||
|
||||
// Result ...
|
||||
type Result struct {
|
||||
obj interface{}
|
||||
similarity float64
|
||||
}
|
||||
|
||||
// All is a fuzzy search.
|
||||
func All(term string, maxUsers, maxAnime, maxPosts, maxThreads, maxTracks, maxCharacters, maxAMVs, maxCompanies int) ([]*arn.User, []*arn.Anime, []*arn.Post, []*arn.Thread, []*arn.SoundTrack, []*arn.Character, []*arn.AMV, []*arn.Company) {
|
||||
if term == "" {
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
userResults []*arn.User
|
||||
animeResults []*arn.Anime
|
||||
postResults []*arn.Post
|
||||
threadResults []*arn.Thread
|
||||
trackResults []*arn.SoundTrack
|
||||
characterResults []*arn.Character
|
||||
amvResults []*arn.AMV
|
||||
companyResults []*arn.Company
|
||||
)
|
||||
|
||||
flow.Parallel(func() {
|
||||
userResults = Users(term, maxUsers)
|
||||
}, func() {
|
||||
animeResults = Anime(term, maxAnime)
|
||||
}, func() {
|
||||
postResults = Posts(term, maxPosts)
|
||||
}, func() {
|
||||
threadResults = Threads(term, maxThreads)
|
||||
}, func() {
|
||||
trackResults = SoundTracks(term, maxTracks)
|
||||
}, func() {
|
||||
characterResults = Characters(term, maxCharacters)
|
||||
}, func() {
|
||||
amvResults = AMVs(term, maxAMVs)
|
||||
}, func() {
|
||||
companyResults = Companies(term, maxCompanies)
|
||||
})
|
||||
|
||||
return userResults, animeResults, postResults, threadResults, trackResults, characterResults, amvResults, companyResults
|
||||
}
|
101
arn/search/Anime.go
Normal file
101
arn/search/Anime.go
Normal file
@ -0,0 +1,101 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
"github.com/animenotifier/notify.moe/arn/stringutils"
|
||||
)
|
||||
|
||||
// Anime searches all anime.
|
||||
func Anime(originalTerm string, maxLength int) []*arn.Anime {
|
||||
term := strings.ToLower(stringutils.RemoveSpecialCharacters(originalTerm))
|
||||
results := make([]*Result, 0, maxLength)
|
||||
|
||||
check := func(text string) float64 {
|
||||
if text == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
return stringutils.AdvancedStringSimilarity(term, strings.ToLower(stringutils.RemoveSpecialCharacters(text)))
|
||||
}
|
||||
|
||||
add := func(anime *arn.Anime, similarity float64) {
|
||||
similarity += float64(anime.Popularity.Total()) * popularityDamping
|
||||
|
||||
results = append(results, &Result{
|
||||
obj: anime,
|
||||
similarity: similarity,
|
||||
})
|
||||
}
|
||||
|
||||
for anime := range arn.StreamAnime() {
|
||||
if anime.ID == originalTerm {
|
||||
return []*arn.Anime{anime}
|
||||
}
|
||||
|
||||
// Canonical title
|
||||
similarity := check(anime.Title.Canonical)
|
||||
|
||||
if similarity >= MinimumStringSimilarity {
|
||||
add(anime, similarity)
|
||||
continue
|
||||
}
|
||||
|
||||
// English
|
||||
similarity = check(anime.Title.English)
|
||||
|
||||
if similarity >= MinimumStringSimilarity {
|
||||
add(anime, similarity)
|
||||
continue
|
||||
}
|
||||
|
||||
// Romaji
|
||||
similarity = check(anime.Title.Romaji)
|
||||
|
||||
if similarity >= MinimumStringSimilarity {
|
||||
add(anime, similarity)
|
||||
continue
|
||||
}
|
||||
|
||||
// Synonyms
|
||||
for _, synonym := range anime.Title.Synonyms {
|
||||
similarity := check(synonym)
|
||||
|
||||
if similarity >= MinimumStringSimilarity {
|
||||
add(anime, similarity)
|
||||
goto nextAnime
|
||||
}
|
||||
}
|
||||
|
||||
// Japanese
|
||||
similarity = check(anime.Title.Japanese)
|
||||
|
||||
if similarity >= MinimumStringSimilarity {
|
||||
add(anime, similarity)
|
||||
continue
|
||||
}
|
||||
|
||||
nextAnime:
|
||||
}
|
||||
|
||||
// Sort
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].similarity > results[j].similarity
|
||||
})
|
||||
|
||||
// Limit
|
||||
if len(results) >= maxLength {
|
||||
results = results[:maxLength]
|
||||
}
|
||||
|
||||
// Final list
|
||||
final := make([]*arn.Anime, len(results))
|
||||
|
||||
for i, result := range results {
|
||||
final[i] = result.obj.(*arn.Anime)
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
55
arn/search/Anime_test.go
Normal file
55
arn/search/Anime_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package search_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/animenotifier/notify.moe/arn/search"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Run these search terms and expect the
|
||||
// anime ID on the right as first result.
|
||||
var tests = map[string]string{
|
||||
"lucky star": "Pg9BcFmig", // Lucky☆Star
|
||||
"dragn bll": "hbih5KmmR", // Dragon Ball
|
||||
"dragon ball": "hbih5KmmR", // Dragon Ball
|
||||
"dragon ball z": "ir-05Fmmg", // Dragon Ball Z
|
||||
"masotan": "grdNhFiiR", // Hisone to Maso-tan
|
||||
"akame": "iEaTpFiig", // Akame ga Kill!
|
||||
"kimi": "7VjCpFiiR", // Kimi no Na wa.
|
||||
"working": "0iIgtFimg", // Working!!
|
||||
"k on": "LP8j5Kmig", // K-On!
|
||||
"ko n": "LP8j5Kmig", // K-On!
|
||||
"kon": "LP8j5Kmig", // K-On!
|
||||
"danmachi": "LTTPtKmiR", // Dungeon ni Deai wo Motomeru no wa Machigatteiru Darou ka
|
||||
"sword oratoria": "ifGetFmig", // Dungeon ni Deai wo Motomeru no wa Machigatteiru Darou ka Gaiden: Sword Oratoria
|
||||
"gint": "QAZ1cKmig", // Gintama
|
||||
"k": "EDSOtKmig", // K
|
||||
"champloo": "0ER25Fiig", // Samurai Champloo
|
||||
"one peace": "jdZp5KmiR", // One Piece
|
||||
"howl": "CpmTcFmig", // Howl's Moving Castle
|
||||
"howl's": "CpmTcFmig", // Howl's Moving Castle
|
||||
"howls": "CpmTcFmig", // Howl's Moving Castle
|
||||
"fate stay": "74y2cFiiR", // Fate/stay night
|
||||
"fate night": "74y2cFiiR", // Fate/stay night
|
||||
"stay night": "74y2cFiiR", // Fate/stay night
|
||||
"re zero": "Un9XpFimg", // Re:Zero kara Hajimeru Isekai Seikatsu
|
||||
}
|
||||
|
||||
func TestAnimeSearch(t *testing.T) {
|
||||
for term, expectedAnimeID := range tests {
|
||||
results := search.Anime(term, 1)
|
||||
assert.Len(t, results, 1, "%s -> %s", term, expectedAnimeID)
|
||||
assert.Equal(t, expectedAnimeID, results[0].ID, "%s -> %s", term, expectedAnimeID)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAnimeSearch(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
search.Anime("drgon bll", 1)
|
||||
}
|
||||
})
|
||||
}
|
112
arn/search/Characters.go
Normal file
112
arn/search/Characters.go
Normal file
@ -0,0 +1,112 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
"github.com/animenotifier/notify.moe/arn/stringutils"
|
||||
)
|
||||
|
||||
// Characters searches all characters.
|
||||
func Characters(originalTerm string, maxLength int) []*arn.Character {
|
||||
if maxLength == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
term := strings.ToLower(stringutils.RemoveSpecialCharacters(originalTerm))
|
||||
termHasUnicode := stringutils.ContainsUnicodeLetters(term)
|
||||
results := make([]*Result, 0, maxLength)
|
||||
|
||||
for character := range arn.StreamCharacters() {
|
||||
if character.ID == originalTerm {
|
||||
return []*arn.Character{character}
|
||||
}
|
||||
|
||||
if character.Image.Extension == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Canonical
|
||||
text := strings.ToLower(stringutils.RemoveSpecialCharacters(character.Name.Canonical))
|
||||
|
||||
if text == term {
|
||||
results = append(results, &Result{
|
||||
obj: character,
|
||||
similarity: float64(20 + len(character.Likes)),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
spaceCount := 0
|
||||
start := 0
|
||||
found := false
|
||||
|
||||
for i := 0; i <= len(text); i++ {
|
||||
if i == len(text) || text[i] == ' ' {
|
||||
part := text[start:i]
|
||||
|
||||
if part == term {
|
||||
results = append(results, &Result{
|
||||
obj: character,
|
||||
similarity: float64(10 - spaceCount*5 + len(character.Likes)),
|
||||
})
|
||||
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
start = i + 1
|
||||
spaceCount++
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
|
||||
// Japanese
|
||||
if termHasUnicode {
|
||||
if strings.Contains(character.Name.Japanese, term) {
|
||||
results = append(results, &Result{
|
||||
obj: character,
|
||||
similarity: float64(len(character.Likes)),
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
similarityA := results[i].similarity
|
||||
similarityB := results[j].similarity
|
||||
|
||||
if similarityA == similarityB {
|
||||
characterA := results[i].obj.(*arn.Character)
|
||||
characterB := results[j].obj.(*arn.Character)
|
||||
|
||||
if characterA.Name.Canonical == characterB.Name.Canonical {
|
||||
return characterA.ID < characterB.ID
|
||||
}
|
||||
|
||||
return characterA.Name.Canonical < characterB.Name.Canonical
|
||||
}
|
||||
|
||||
return similarityA > similarityB
|
||||
})
|
||||
|
||||
// Limit
|
||||
if len(results) >= maxLength {
|
||||
results = results[:maxLength]
|
||||
}
|
||||
|
||||
// Final list
|
||||
final := make([]*arn.Character, len(results))
|
||||
|
||||
for i, result := range results {
|
||||
final[i] = result.obj.(*arn.Character)
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
54
arn/search/Companies.go
Normal file
54
arn/search/Companies.go
Normal file
@ -0,0 +1,54 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
"github.com/animenotifier/notify.moe/arn/stringutils"
|
||||
)
|
||||
|
||||
// Companies searches all companies.
|
||||
func Companies(originalTerm string, maxLength int) []*arn.Company {
|
||||
term := strings.ToLower(stringutils.RemoveSpecialCharacters(originalTerm))
|
||||
results := make([]*Result, 0, maxLength)
|
||||
|
||||
for company := range arn.StreamCompanies() {
|
||||
if company.ID == originalTerm {
|
||||
return []*arn.Company{company}
|
||||
}
|
||||
|
||||
if company.IsDraft {
|
||||
continue
|
||||
}
|
||||
|
||||
text := strings.ToLower(stringutils.RemoveSpecialCharacters(company.Name.English))
|
||||
similarity := stringutils.AdvancedStringSimilarity(term, text)
|
||||
|
||||
if similarity >= MinimumStringSimilarity {
|
||||
results = append(results, &Result{
|
||||
obj: company,
|
||||
similarity: similarity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].similarity > results[j].similarity
|
||||
})
|
||||
|
||||
// Limit
|
||||
if len(results) >= maxLength {
|
||||
results = results[:maxLength]
|
||||
}
|
||||
|
||||
// Final list
|
||||
final := make([]*arn.Company, len(results))
|
||||
|
||||
for i, result := range results {
|
||||
final[i] = result.obj.(*arn.Company)
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
41
arn/search/Posts.go
Normal file
41
arn/search/Posts.go
Normal file
@ -0,0 +1,41 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
"github.com/animenotifier/notify.moe/arn/stringutils"
|
||||
)
|
||||
|
||||
// Posts searches all posts.
|
||||
func Posts(originalTerm string, maxLength int) []*arn.Post {
|
||||
term := strings.ToLower(stringutils.RemoveSpecialCharacters(originalTerm))
|
||||
results := make([]*arn.Post, 0, maxLength)
|
||||
|
||||
for post := range arn.StreamPosts() {
|
||||
if post.ID == originalTerm {
|
||||
return []*arn.Post{post}
|
||||
}
|
||||
|
||||
text := strings.ToLower(post.Text)
|
||||
|
||||
if !strings.Contains(text, term) {
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, post)
|
||||
}
|
||||
|
||||
// Sort
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Created > results[j].Created
|
||||
})
|
||||
|
||||
// Limit
|
||||
if len(results) >= maxLength {
|
||||
results = results[:maxLength]
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
66
arn/search/SoundTracks.go
Normal file
66
arn/search/SoundTracks.go
Normal file
@ -0,0 +1,66 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
"github.com/animenotifier/notify.moe/arn/stringutils"
|
||||
)
|
||||
|
||||
// SoundTracks searches all soundtracks.
|
||||
func SoundTracks(originalTerm string, maxLength int) []*arn.SoundTrack {
|
||||
term := strings.ToLower(stringutils.RemoveSpecialCharacters(originalTerm))
|
||||
results := make([]*Result, 0, maxLength)
|
||||
|
||||
for track := range arn.StreamSoundTracks() {
|
||||
if track.ID == originalTerm {
|
||||
return []*arn.SoundTrack{track}
|
||||
}
|
||||
|
||||
if track.IsDraft {
|
||||
continue
|
||||
}
|
||||
|
||||
text := strings.ToLower(track.Title.Canonical)
|
||||
similarity := stringutils.AdvancedStringSimilarity(term, text)
|
||||
|
||||
if similarity >= MinimumStringSimilarity {
|
||||
results = append(results, &Result{
|
||||
obj: track,
|
||||
similarity: similarity,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
text = strings.ToLower(track.Title.Native)
|
||||
similarity = stringutils.AdvancedStringSimilarity(term, text)
|
||||
|
||||
if similarity >= MinimumStringSimilarity {
|
||||
results = append(results, &Result{
|
||||
obj: track,
|
||||
similarity: similarity,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Sort
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].similarity > results[j].similarity
|
||||
})
|
||||
|
||||
// Limit
|
||||
if len(results) >= maxLength {
|
||||
results = results[:maxLength]
|
||||
}
|
||||
|
||||
// Final list
|
||||
final := make([]*arn.SoundTrack, len(results))
|
||||
|
||||
for i, result := range results {
|
||||
final[i] = result.obj.(*arn.SoundTrack)
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
47
arn/search/Threads.go
Normal file
47
arn/search/Threads.go
Normal file
@ -0,0 +1,47 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
"github.com/animenotifier/notify.moe/arn/stringutils"
|
||||
)
|
||||
|
||||
// Threads searches all threads.
|
||||
func Threads(originalTerm string, maxLength int) []*arn.Thread {
|
||||
term := strings.ToLower(stringutils.RemoveSpecialCharacters(originalTerm))
|
||||
results := make([]*arn.Thread, 0, maxLength)
|
||||
|
||||
for thread := range arn.StreamThreads() {
|
||||
if thread.ID == originalTerm {
|
||||
return []*arn.Thread{thread}
|
||||
}
|
||||
|
||||
text := strings.ToLower(thread.Text)
|
||||
|
||||
if strings.Contains(text, term) {
|
||||
results = append(results, thread)
|
||||
continue
|
||||
}
|
||||
|
||||
text = strings.ToLower(thread.Title)
|
||||
|
||||
if strings.Contains(text, term) {
|
||||
results = append(results, thread)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Sort
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Created > results[j].Created
|
||||
})
|
||||
|
||||
// Limit
|
||||
if len(results) >= maxLength {
|
||||
results = results[:maxLength]
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
54
arn/search/Users.go
Normal file
54
arn/search/Users.go
Normal file
@ -0,0 +1,54 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/animenotifier/notify.moe/arn"
|
||||
"github.com/animenotifier/notify.moe/arn/stringutils"
|
||||
)
|
||||
|
||||
// Users searches all users.
|
||||
func Users(originalTerm string, maxLength int) []*arn.User {
|
||||
term := strings.ToLower(stringutils.RemoveSpecialCharacters(originalTerm))
|
||||
results := make([]*Result, 0, maxLength)
|
||||
|
||||
for user := range arn.StreamUsers() {
|
||||
if user.ID == originalTerm {
|
||||
return []*arn.User{user}
|
||||
}
|
||||
|
||||
text := strings.ToLower(user.Nick)
|
||||
|
||||
// Similarity check
|
||||
similarity := stringutils.AdvancedStringSimilarity(term, text)
|
||||
|
||||
if similarity < MinimumStringSimilarity {
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, &Result{
|
||||
obj: user,
|
||||
similarity: similarity,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].similarity > results[j].similarity
|
||||
})
|
||||
|
||||
// Limit
|
||||
if len(results) >= maxLength {
|
||||
results = results[:maxLength]
|
||||
}
|
||||
|
||||
// Final list
|
||||
final := make([]*arn.User, len(results))
|
||||
|
||||
for i, result := range results {
|
||||
final[i] = result.obj.(*arn.User)
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
Reference in New Issue
Block a user