Added arn to the main repository

This commit is contained in:
2019-06-03 18:32:43 +09:00
parent cf258573a8
commit 29a48d94a5
465 changed files with 15968 additions and 288 deletions

66
arn/search/AMVs.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}