Refactor with ordered maps

This commit is contained in:
Ben Sarmiento
2023-11-10 19:03:07 +01:00
parent 15a0ba95d8
commit b97f859a32
12 changed files with 180 additions and 256 deletions

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"path"
"path/filepath"
"strings"
"github.com/debridmediamanager.com/zurg/internal/config"
@@ -69,10 +70,28 @@ func handleListOfTorrents(requestPath string, w http.ResponseWriter, r *http.Req
for _, directory := range c.GetDirectories() {
if basePath == directory {
torrents := t.GetByDirectory(basePath)
resp, err := createMultiTorrentResponse("/"+basePath, torrents)
if err != nil {
return nil, fmt.Errorf("cannot read directory (%s): %w", basePath, err)
var responses []dav.Response
responses = append(responses, dav.Directory(basePath))
for el := t.TorrentMap.Front(); el != nil; el = el.Next() {
accessKey := el.Key
torrent := el.Value
if torrent.InProgress {
continue
}
for _, dir := range torrent.Directories {
if dir == basePath {
path := filepath.Join(basePath, accessKey)
responses = append(responses, dav.Directory(path))
break
}
}
}
resp := &dav.MultiStatus{
XMLNS: "DAV:",
Response: responses,
}
return xml.Marshal(resp)
}
@@ -82,17 +101,37 @@ func handleListOfTorrents(requestPath string, w http.ResponseWriter, r *http.Req
}
func handleSingleTorrent(requestPath string, w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager) ([]byte, error) {
directory := path.Dir(requestPath)
torrentName := path.Base(requestPath)
sameNameTorrents := t.FindAllTorrentsWithName(directory, torrentName)
if len(sameNameTorrents) == 0 {
return nil, fmt.Errorf("cannot find directory when generating single torrent: %s", requestPath)
accessKey := path.Base(requestPath)
torrent, exists := t.TorrentMap.Get(accessKey)
if !exists {
return nil, fmt.Errorf("cannot find torrent %s", requestPath)
}
resp, err := createSingleTorrentResponse("/"+directory, sameNameTorrents)
if err != nil {
return nil, fmt.Errorf("cannot read directory (%s): %w", requestPath, err)
var responses []dav.Response
// initial response is the directory itself
responses = append(responses, dav.Directory(requestPath))
for el := torrent.SelectedFiles.Front(); el != nil; el = el.Next() {
file := el.Value
if file.Link == "" {
// will be caught by torrent manager's repairAll
// just skip it for now
continue
}
filename := filepath.Base(file.Path)
filePath := filepath.Join(requestPath, filename)
responses = append(responses, dav.File(
filePath,
file.Bytes,
convertRFC3339toRFC1123(torrent.LatestAdded),
file.Link,
))
}
resp := &dav.MultiStatus{
XMLNS: "DAV:",
Response: responses,
}
return xml.Marshal(resp)
}

View File

@@ -1,79 +0,0 @@
package dav
import (
"path/filepath"
"github.com/debridmediamanager.com/zurg/internal/torrent"
"github.com/debridmediamanager.com/zurg/pkg/dav"
)
// createMultiTorrentResponse creates a WebDAV response for a list of torrents
func createMultiTorrentResponse(basePath string, torrents []torrent.Torrent) (*dav.MultiStatus, error) {
var responses []dav.Response
responses = append(responses, dav.Directory(basePath))
seen := make(map[string]bool)
for _, item := range torrents {
if item.InProgress {
continue
}
if _, exists := seen[item.AccessKey]; exists {
continue
}
seen[item.AccessKey] = true
path := filepath.Join(basePath, item.AccessKey)
responses = append(responses, dav.Directory(path))
}
return &dav.MultiStatus{
XMLNS: "DAV:",
Response: responses,
}, nil
}
// createTorrentResponse creates a WebDAV response for a single torrent
// but it also handles the case where there are many torrents with the same name
func createSingleTorrentResponse(basePath string, torrents []torrent.Torrent) (*dav.MultiStatus, error) {
var responses []dav.Response
// initial response is the directory itself
currentPath := filepath.Join(basePath, torrents[0].AccessKey)
responses = append(responses, dav.Directory(currentPath))
finalName := make(map[string]bool)
var torrentResponses []dav.Response
for _, torrent := range torrents {
for _, file := range torrent.SelectedFiles {
if file.Link == "" {
// TODO: fix the file?
// log.Println("File has no link, skipping (repairing links take time)", file.Path)
continue
}
filename := filepath.Base(file.Path)
if finalName[filename] {
continue
}
finalName[filename] = true
filePath := filepath.Join(currentPath, filename)
torrentResponses = append(torrentResponses, dav.File(
filePath,
file.Bytes,
convertRFC3339toRFC1123(torrent.LatestAdded),
file.Link,
))
}
}
responses = append(responses, torrentResponses...)
return &dav.MultiStatus{
XMLNS: "DAV:",
Response: responses,
}, nil
}

View File

@@ -65,13 +65,15 @@ func handleListOfTorrents(requestPath string, w http.ResponseWriter, r *http.Req
for _, directory := range c.GetDirectories() {
if basePath == directory {
htmlDoc := "<ol>"
for name, torrent := range t.TorrentMap {
if len(torrent.SelectedFiles) == 0 {
for el := t.TorrentMap.Front(); el != nil; el = el.Next() {
accessKey := el.Key
torrent := el.Value
if torrent.InProgress {
continue
}
for _, dir := range torrent.Directories {
if dir == basePath {
htmlDoc += fmt.Sprintf("<li><a href=\"%s/\">%s</a></li>", filepath.Join(requestPath, url.PathEscape(name)), name)
htmlDoc += fmt.Sprintf("<li><a href=\"%s/\">%s</a></li>", filepath.Join(requestPath, url.PathEscape(accessKey)), accessKey)
break
}
}
@@ -84,12 +86,18 @@ func handleListOfTorrents(requestPath string, w http.ResponseWriter, r *http.Req
}
func handleSingleTorrent(requestPath string, w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager) (*string, error) {
torrentName := path.Base(requestPath)
accessKey := path.Base(requestPath)
torrent, _ := t.TorrentMap.Get(accessKey)
if torrent == nil {
return nil, fmt.Errorf("cannot find torrent %s", requestPath)
}
htmlDoc := "<ol>"
for _, file := range t.TorrentMap[torrentName].SelectedFiles {
for el := torrent.SelectedFiles.Front(); el != nil; el = el.Next() {
file := el.Value
if file.Link == "" {
// TODO: fix the file?
fmt.Printf("File %s has no link, skipping\n", file.Path)
// will be caught by torrent manager's repairAll
// just skip it for now
continue
}
filename := filepath.Base(file.Path)

View File

@@ -3,7 +3,9 @@ package torrent
import (
"encoding/gob"
"fmt"
"math"
"os"
"path/filepath"
"strings"
"sync"
"time"
@@ -11,18 +13,17 @@ import (
"github.com/debridmediamanager.com/zurg/internal/config"
"github.com/debridmediamanager.com/zurg/pkg/logutil"
"github.com/debridmediamanager.com/zurg/pkg/realdebrid"
"github.com/hashicorp/golang-lru/v2/expirable"
"github.com/elliotchance/orderedmap/v2"
"github.com/nutsdb/nutsdb"
"go.uber.org/zap"
)
type TorrentManager struct {
TorrentMap map[string]*Torrent
TorrentMap *orderedmap.OrderedMap[string, *Torrent] // accessKey -> Torrent
requiredVersion string
rd *realdebrid.RealDebrid
checksum string
config config.ConfigInterface
cache *expirable.LRU[string, string]
db *nutsdb.DB
workerPool chan bool
directoryMap map[string][]string
@@ -34,13 +35,12 @@ type TorrentManager struct {
// NewTorrentManager creates a new torrent manager
// it will fetch all torrents and their info in the background
// and store them in-memory and cached in files
func NewTorrentManager(config config.ConfigInterface, cache *expirable.LRU[string, string], db *nutsdb.DB) *TorrentManager {
func NewTorrentManager(config config.ConfigInterface, db *nutsdb.DB) *TorrentManager {
t := &TorrentManager{
TorrentMap: make(map[string]*Torrent),
requiredVersion: fmt.Sprintf("8.11.2023 - retain:%v", config.EnableRetainFolderNameExtension()),
TorrentMap: orderedmap.NewOrderedMap[string, *Torrent](),
requiredVersion: fmt.Sprintf("10.11.2023/retain=%t", config.EnableRetainFolderNameExtension()),
rd: realdebrid.NewRealDebrid(config.GetToken(), logutil.NewLogger().Named("realdebrid")),
config: config,
cache: cache,
db: db,
workerPool: make(chan bool, config.GetNumOfWorkers()),
directoryMap: make(map[string][]string),
@@ -74,10 +74,11 @@ func NewTorrentManager(config config.ConfigInterface, cache *expirable.LRU[strin
if newTorrent == nil {
continue
}
if _, exists := t.TorrentMap[newTorrent.AccessKey]; exists {
t.TorrentMap[newTorrent.AccessKey] = t.mergeToMain(t.TorrentMap[newTorrent.AccessKey], newTorrent)
torrent, _ := t.TorrentMap.Get(newTorrent.AccessKey)
if torrent != nil {
t.TorrentMap.Set(newTorrent.AccessKey, t.mergeToMain(torrent, newTorrent))
} else {
t.TorrentMap[newTorrent.AccessKey] = newTorrent
t.TorrentMap.Set(newTorrent.AccessKey, newTorrent)
}
}
t.checksum = t.getChecksum()
@@ -95,15 +96,11 @@ func (t *TorrentManager) mergeToMain(t1, t2 *Torrent) *Torrent {
merged := t1
// Merge SelectedFiles
fileMap := make(map[int]File)
for _, f := range append(t1.SelectedFiles, t2.SelectedFiles...) {
if _, exists := fileMap[f.ID]; !exists {
fileMap[f.ID] = f
for el := t2.SelectedFiles.Front(); el != nil; el = el.Next() {
if _, ok := merged.SelectedFiles.Get(el.Key); !ok {
merged.SelectedFiles.Set(el.Key, el.Value)
}
}
for _, f := range fileMap {
merged.SelectedFiles = append(merged.SelectedFiles, f)
}
// Merge Instances
merged.Instances = append(t1.Instances, t2.Instances...)
@@ -113,7 +110,7 @@ func (t *TorrentManager) mergeToMain(t1, t2 *Torrent) *Torrent {
merged.LatestAdded = t2.LatestAdded
}
// InProgress
// InProgress - if one of the instances is in progress, then the whole torrent is in progress
for _, instance := range merged.Instances {
if instance.Progress != 100 {
merged.InProgress = true
@@ -124,36 +121,6 @@ func (t *TorrentManager) mergeToMain(t1, t2 *Torrent) *Torrent {
return merged
}
// GetByDirectory returns all torrents that have a file in the specified directory
func (t *TorrentManager) GetByDirectory(directory string) []Torrent {
var torrents []Torrent
for k, v := range t.TorrentMap {
found := false
for _, dir := range v.Directories {
if dir == directory {
found = true
break
}
}
if found {
torrents = append(torrents, *t.TorrentMap[k])
}
}
return torrents
}
// FindAllTorrentsWithName finds all torrents in a given directory with a given name
func (t *TorrentManager) FindAllTorrentsWithName(directory, torrentName string) []Torrent {
var matchingTorrents []Torrent
torrents := t.GetByDirectory(directory)
for i := range torrents {
if torrents[i].AccessKey == torrentName || strings.Contains(torrents[i].AccessKey, torrentName) {
matchingTorrents = append(matchingTorrents, torrents[i])
}
}
return matchingTorrents
}
// proxy
func (t *TorrentManager) UnrestrictUntilOk(link string) *realdebrid.UnrestrictResponse {
return t.rd.UnrestrictUntilOk(link)
@@ -244,16 +211,33 @@ func (t *TorrentManager) startRefreshJob() {
<-t.workerPool
}(i)
}
// deletes
// for el := t.TorrentMap.Front(); el != nil; el = el.Next() {
// found := false
// for _, newTorrent := range newTorrents {
// if newTorrent.ID == el.Value.AccessKey {
// found = true
// break
// }
// }
// if !found {
// t.log.Infof("Torrent id=%s is no longer found", accessKey)
// t.TorrentMap.Delete(accessKey)
// }
// }
wg.Wait()
close(torrentsChan)
for newTorrent := range torrentsChan {
if newTorrent == nil {
continue
}
if _, exists := t.TorrentMap[newTorrent.AccessKey]; exists {
t.TorrentMap[newTorrent.AccessKey] = t.mergeToMain(t.TorrentMap[newTorrent.AccessKey], newTorrent)
torrent, _ := t.TorrentMap.Get(newTorrent.AccessKey)
if torrent != nil {
t.TorrentMap.Set(newTorrent.AccessKey, t.mergeToMain(torrent, newTorrent))
} else {
t.TorrentMap[newTorrent.AccessKey] = newTorrent
t.TorrentMap.Set(newTorrent.AccessKey, newTorrent)
}
}
t.checksum = t.getChecksum()
@@ -288,7 +272,7 @@ func (t *TorrentManager) getMoreInfo(rdTorrent realdebrid.Torrent) *Torrent {
// it also has a Link field, which can be empty
// if it is empty, it means the file is no longer available
// Files+Links together are the same as SelectedFiles
var selectedFiles []File
selectedFiles := orderedmap.NewOrderedMap[string, *File]()
streamableCount := 0
// if some Links are empty, we need to repair it
forRepair := false
@@ -299,22 +283,26 @@ func (t *TorrentManager) getMoreInfo(rdTorrent realdebrid.Torrent) *Torrent {
if file.Selected == 0 {
continue
}
selectedFiles = append(selectedFiles, File{
selectedFiles.Set(filepath.Base(file.Path), &File{
File: file,
Link: "", // no link yet
})
}
if len(selectedFiles) > len(info.Links) && info.Progress == 100 {
t.log.Debugf("Some links has expired for %s %s: %d selected but only %d link(s)", info.ID, info.Name, len(selectedFiles), len(info.Links))
if selectedFiles.Len() > len(info.Links) && info.Progress == 100 {
t.log.Debugf("Some links has expired for %s %s: %d selected but only %d link(s)", info.ID, info.Name, selectedFiles.Len(), len(info.Links))
// chaotic file means RD will not output the desired file selection
// e.g. even if we select just a single mkv, it will output a rar
var isChaotic bool
selectedFiles, isChaotic = t.organizeChaos(&rdTorrent, selectedFiles)
if isChaotic {
if isChaotic && selectedFiles.Len() == 1 {
t.log.Infof("Torrent %s %s is unfixable, it's always returning an unstreamable link, ignoring", info.ID, info.Name)
t.log.Debugf("You can try fixing it yourself magnet:?xt=urn:btih:%s", info.Hash)
} else {
if streamableCount > 1 {
// case for repair 1: it's missing some links (or all links)
// if we download it as is, we might get the same file over and over again
// so we need to redownload it with other files selected
// that is why we check if there are other streamable files
t.log.Infof("Torrent %s %s marked for repair", info.ID, info.Name)
forRepair = true
} else {
@@ -322,10 +310,16 @@ func (t *TorrentManager) getMoreInfo(rdTorrent realdebrid.Torrent) *Torrent {
t.log.Debugf("You can try fixing it yourself magnet:?xt=urn:btih:%s", info.Hash)
}
}
} else if len(selectedFiles) > 0 {
} else if selectedFiles.Len() == len(info.Links) {
// all links are still intact! good!
for i, link := range info.Links {
selectedFiles[i].Link = link
i := 0
for el := selectedFiles.Front(); el != nil; el = el.Next() {
if i < len(info.Links) {
file := el.Value
file.Link = info.Links[i]
selectedFiles.Set(el.Key, file)
i++
}
}
}
@@ -338,7 +332,7 @@ func (t *TorrentManager) getMoreInfo(rdTorrent realdebrid.Torrent) *Torrent {
InProgress: info.Progress != 100,
Instances: []realdebrid.TorrentInfo{*info},
}
if len(selectedFiles) > 0 && torrentFromFile == nil {
if selectedFiles.Len() > 0 && torrentFromFile == nil {
t.writeToFile(info) // only when there are selected files, else it's useless
}
return &torrent
@@ -429,7 +423,7 @@ func (t *TorrentManager) readFromFile(torrentID string) *realdebrid.TorrentInfo
return &torrent
}
func (t *TorrentManager) organizeChaos(info *realdebrid.Torrent, selectedFiles []File) ([]File, bool) {
func (t *TorrentManager) organizeChaos(info *realdebrid.Torrent, selectedFiles *orderedmap.OrderedMap[string, *File]) (*orderedmap.OrderedMap[string, *File], bool) {
type Result struct {
Response *realdebrid.UnrestrictResponse
}
@@ -465,84 +459,61 @@ func (t *TorrentManager) organizeChaos(info *realdebrid.Torrent, selectedFiles [
continue
}
found := false
for i := range selectedFiles {
if strings.Contains(selectedFiles[i].Path, result.Response.Filename) {
for el := selectedFiles.Front(); el != nil; el = el.Next() {
if file, _ := selectedFiles.Get(el.Key); strings.Contains(file.Path, result.Response.Filename) {
t.log.Debugf("Found a file that is in the selection for torrent id=%s: %s", info.ID, result.Response.Filename)
selectedFiles[i].Link = result.Response.Link
file.Link = result.Response.Link
found = true
}
}
if !found {
isChaotic = result.Response.Streamable == 0
t.log.Debugf("Found a file that is not in the selection for torrent id=%s: %s %v", info.ID, result.Response.Filename, result.Response.Streamable)
selectedFiles = append(selectedFiles, File{
File: realdebrid.File{
Path: result.Response.Filename,
Bytes: result.Response.Filesize,
Selected: 1,
},
Link: result.Response.Link,
})
t.log.Debugf("Found a file that is NOT in the selection for torrent id=%s: %s %v", info.ID, result.Response.Filename, result.Response.Streamable)
if result.Response.Streamable == 1 {
selectedFiles.Set(filepath.Base(result.Response.Filename), &File{
File: realdebrid.File{
ID: math.MaxInt32,
Path: result.Response.Filename,
Bytes: result.Response.Filesize,
Selected: 1,
},
Link: result.Response.Link,
})
}
}
}
return selectedFiles, isChaotic
}
// HideTheFile marks a file as deleted
// func (t *TorrentManager) HideTheFile(torrent *Torrent, file *File) {
// file.Unavailable = true
// t.repair(torrent, false)
// }
// func (t *TorrentManager) repairAll() {
// for _, torrent := range t.torrentMap {
// // do not repair if:
// // in progress
// hasInProgress := false
// for _, info := range torrent.Instances {
// if info.Progress != 100 {
// hasInProgress = true
// break
// }
// }
// if hasInProgress {
// for el := t.TorrentMap.Front(); el != nil; el = el.Next() {
// torrent := el.Value
// // do not repair if: in progress
// if torrent.InProgress {
// continue
// }
// // already repaired based on other instances
// var missingFiles []File
// for _, file := range torrent.SelectedFiles {
// if file.Link == "" || file.Unavailable {
// missingFiles = append(missingFiles, file)
// for el2 := torrent.SelectedFiles.Front(); el2 != nil; el2 = el2.Next() {
// file, ok := torrent.SelectedFiles.Get(el2.Key)
// if !ok {
// continue
// }
// }
// for _, sFile := range selectedFiles {
// if sFile.Link == "" || sFile.Unavailable {
// found := false
// for _, fFile := range foundFiles {
// // same file but different link, then yes it has been repaired
// if sFile.Path == fFile.Path && sFile.Link != fFile.Link {
// found = true
// break
// }
// }
// if !found {
// missingFiles = append(missingFiles, sFile)
// }
// // check for case of repairs like
// // case 1: missing links
// // case 2: unrestrictable links TODO
// if file.Link == "" {
// missingFiles = append(missingFiles, *file)
// }
// }
// if len(missingFiles) == 0 {
// t.log.Infof("Torrent id=%s is already repaired", info.ID)
// return
// continue
// }
// for _, info := range torrent.Instances {
// if info.Progress != 100 {
// continue
// }
// if info.ForRepair {
// t.log.Infof("There were less links than was expected on %s %s; fixing...", info.ID, info.Name)
// t.repair(&info, true)
// // t.repair(&info, true)
// break // only repair the first one for repair and then move on
// }
// if len(info.Links) == 0 && info.Progress == 100 {
@@ -557,6 +528,7 @@ func (t *TorrentManager) organizeChaos(info *realdebrid.Torrent, selectedFiles [
// }
// func (t *TorrentManager) repair(info *realdebrid.TorrentInfo, tryReinsertionFirst bool) {
// // file.Link == "" should be repaired
// // then we repair it!
// t.log.Infof("Repairing torrent id=%s", info.ID)
// // check if we can still add more downloads
@@ -576,6 +548,9 @@ func (t *TorrentManager) organizeChaos(info *realdebrid.Torrent, selectedFiles [
// var otherStreamableFileIDs []int
// for _, file := range info.Files {
// found := false
// for el := selectedFiles.Front(); el != nil; el = el.Next() {
// }
// for _, selectedFile := range selectedFiles {
// if selectedFile.ID == file.ID {
// found = true

View File

@@ -2,11 +2,12 @@ package torrent
import (
"github.com/debridmediamanager.com/zurg/pkg/realdebrid"
"github.com/elliotchance/orderedmap/v2"
)
type Torrent struct {
AccessKey string
SelectedFiles []File
SelectedFiles *orderedmap.OrderedMap[string, *File]
Directories []string
LatestAdded string
InProgress bool
@@ -16,6 +17,5 @@ type Torrent struct {
type File struct {
realdebrid.File
Link string
Unavailable bool
Link string
}

View File

@@ -47,17 +47,17 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent
}
baseDirectory := segments[len(segments)-3]
torrentName := segments[len(segments)-2]
accessKey := segments[len(segments)-2]
filename := segments[len(segments)-1]
torrents := t.FindAllTorrentsWithName(baseDirectory, torrentName)
if torrents == nil {
log.Errorf("Cannot find torrent %s in the directory %s", requestPath, baseDirectory)
torrent, _ := t.TorrentMap.Get(accessKey)
if torrent == nil {
log.Errorf("Cannot find torrent %s in the directory %s", accessKey, baseDirectory)
http.Error(w, "File not found", http.StatusNotFound)
return
}
_, file := getFile(torrents, filename)
file, _ := torrent.SelectedFiles.Get(filename)
if file == nil {
log.Errorf("Cannot find file from path %s", requestPath)
http.Error(w, "File not found", http.StatusNotFound)
@@ -73,10 +73,7 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent
resp := t.UnrestrictUntilOk(link)
if resp == nil {
if !file.Unavailable {
log.Errorf("Cannot unrestrict file %s %s", filename, link)
// t.HideTheFile(torrent, file)
}
// TODO: maybe repair the torrent?
streamErrorVideo("https://www.youtube.com/watch?v=gea_FJrtFVA", w, r, t, c, log)
return
} else if resp.Filename != filename {

View File

@@ -41,17 +41,17 @@ func HandleHeadRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torren
}
baseDirectory := segments[len(segments)-3]
torrentName := segments[len(segments)-2]
accessKey := segments[len(segments)-2]
filename := segments[len(segments)-1]
torrents := t.FindAllTorrentsWithName(baseDirectory, torrentName)
if torrents == nil {
log.Errorf("Cannot find torrent %s in the directory %s", requestPath, baseDirectory)
http.Error(w, "Cannot find file", http.StatusNotFound)
torrent, _ := t.TorrentMap.Get(accessKey)
if torrent == nil {
log.Errorf("Cannot find torrent %s in the directory %s", accessKey, baseDirectory)
http.Error(w, "File not found", http.StatusNotFound)
return
}
_, file := getFile(torrents, filename)
file, _ := torrent.SelectedFiles.Get(filename)
if file == nil {
log.Errorf("Cannot find file from path %s", requestPath)
http.Error(w, "Cannot find file", http.StatusNotFound)

View File

@@ -1,20 +1 @@
package universal
import (
"path/filepath"
"github.com/debridmediamanager.com/zurg/internal/torrent"
)
// getFile finds a link by a fragment, it might be wrong
func getFile(torrents []torrent.Torrent, filename string) (*torrent.Torrent, *torrent.File) {
for t := range torrents {
for f, file := range torrents[t].SelectedFiles {
fname := filepath.Base(file.Path)
if filename == fname {
return &torrents[t], &torrents[t].SelectedFiles[f]
}
}
}
return nil, nil
}