From 1d040e0d70299f1fdc93b7332ca7dc6315e36613 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Fri, 3 Jan 2025 19:39:47 -0300 Subject: [PATCH 1/4] bumped go to version 1.23.4, updated packages too --- go.mod | 21 ++++++----- go.sum | 110 +++++++++++++++++++++------------------------------------ 2 files changed, 51 insertions(+), 80 deletions(-) diff --git a/go.mod b/go.mod index cf97865..f9fb008 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,25 @@ module github.com/alvarorichard/Goanime -go 1.23.3 +go 1.23.4 require ( - github.com/PuerkitoBio/goquery v1.10.0 + github.com/PuerkitoBio/goquery v1.10.1 github.com/charmbracelet/bubbles v0.20.0 - github.com/charmbracelet/bubbletea v1.2.3 + github.com/charmbracelet/bubbletea v1.2.4 github.com/charmbracelet/lipgloss v1.0.0 github.com/hugolgst/rich-go v0.0.0-20240715122152-74618cc1ace2 github.com/ktr0731/go-fuzzyfinder v0.8.0 github.com/manifoldco/promptui v0.9.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.10.0 - golang.org/x/net v0.31.0 + golang.org/x/net v0.33.0 ) require ( - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/andybalholm/cascadia v1.3.2 // indirect + github.com/andybalholm/cascadia v1.3.3 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.5.2 // indirect + github.com/charmbracelet/x/ansi v0.6.0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -39,10 +38,10 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/stretchr/objx v0.5.2 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/term v0.26.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 85cf466..5b8f88c 100644 --- a/go.sum +++ b/go.sum @@ -1,41 +1,19 @@ -github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= -github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= -github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= -github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU= +github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= -github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= -github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= -github.com/charmbracelet/bubbletea v1.2.0 h1:WYHclJaFDOz4dPxiGx7owwb8P4000lYPcuXPIALS5Z8= -github.com/charmbracelet/bubbletea v1.2.0/go.mod h1:viLoDL7hG4njLJSKU2gw7kB3LSEmWsrM80rO1dBJWBI= -github.com/charmbracelet/bubbletea v1.2.3 h1:d9MdMsANIYZB5pE1KkRqaUV6GfsiWm+/9z4fTuGVm9I= -github.com/charmbracelet/bubbletea v1.2.3/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= +github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= -github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= -github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= -github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= -github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E= -github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= -github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA= +github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -49,8 +27,6 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= @@ -69,8 +45,6 @@ github.com/ktr0731/go-ansisgr v0.1.0 h1:fbuupput8739hQbEmZn1cEKjqQFwtCCZNznnF6AN github.com/ktr0731/go-ansisgr v0.1.0/go.mod h1:G9lxwgBwH0iey0Dw5YQd7n6PmQTwTuTM/X5Sgm/UrzE= github.com/ktr0731/go-fuzzyfinder v0.8.0 h1:+yobwo9lqZZ7jd1URPdCgZXTE2U1mpIVTkQoo4roi6w= github.com/ktr0731/go-fuzzyfinder v0.8.0/go.mod h1:Bjpz5im+tppKE9Ii6UK1h+6RaX/lUvJ0ruO4LIYRkqo= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= @@ -100,42 +74,40 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -146,38 +118,38 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From d8fb3dd58516a4b2be010629e3fd1db520126534 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Fri, 3 Jan 2025 20:23:25 -0300 Subject: [PATCH 2/4] The shebang must be on the first line. Deleted blanks. --- build/buildlinux.sh | 3 --- build/buildmacos.sh | 4 ---- build/buildwindows.sh | 2 -- 3 files changed, 9 deletions(-) diff --git a/build/buildlinux.sh b/build/buildlinux.sh index 747169f..75e544d 100644 --- a/build/buildlinux.sh +++ b/build/buildlinux.sh @@ -1,6 +1,3 @@ - - - #!/bin/bash # Exit immediately if a command exits with a non-zero status diff --git a/build/buildmacos.sh b/build/buildmacos.sh index 0d51aee..495aedd 100755 --- a/build/buildmacos.sh +++ b/build/buildmacos.sh @@ -1,7 +1,3 @@ - - - - #!/bin/bash # Exit immediately if a command exits with a non-zero status diff --git a/build/buildwindows.sh b/build/buildwindows.sh index 78c79ec..721d024 100755 --- a/build/buildwindows.sh +++ b/build/buildwindows.sh @@ -1,5 +1,3 @@ - - #!/bin/bash # Sai imediatamente se um comando falhar From c59044c2a32eae6f2afec3379351b598c9c37e8a Mon Sep 17 00:00:00 2001 From: Eduardo Date: Fri, 3 Jan 2025 20:59:37 -0300 Subject: [PATCH 3/4] Moved many blocks of code from player.go to new files --- internal/api/discord.go | 56 ---- internal/player/discord.go | 138 ++++++++ internal/player/helper.go | 116 +++++++ internal/player/player.go | 630 +------------------------------------ internal/player/scraper.go | 318 +++++++++++++++++++ 5 files changed, 573 insertions(+), 685 deletions(-) delete mode 100644 internal/api/discord.go create mode 100644 internal/player/discord.go create mode 100644 internal/player/helper.go create mode 100644 internal/player/scraper.go diff --git a/internal/api/discord.go b/internal/api/discord.go deleted file mode 100644 index 30c03a4..0000000 --- a/internal/api/discord.go +++ /dev/null @@ -1,56 +0,0 @@ -package api - -import ( - "fmt" - "github.com/alvarorichard/Goanime/internal/util" - "github.com/hugolgst/rich-go/client" - "log" -) - -// DiscordPresence updates Discord Rich Presence with anime details and cover link -func DiscordPresence(clientId string, anime Anime, isPaused bool, timestamp int64) error { - // Login to Discord - err := client.Login(clientId) - if err != nil { - return fmt.Errorf("failed to login to Discord: %w", err) - } - - // Define the state message based on whether the episode is paused - var state string - if isPaused { - state = fmt.Sprintf("Watching Episode %d (Paused)", anime.Episodes[0].Num) - } else { - state = fmt.Sprintf("Watching Episode %d", anime.Episodes[0].Num) - } - - // Set up the activity for Discord Rich Presence without a LargeImage key - activity := client.Activity{ - Details: anime.Name, - LargeImage: anime.ImageURL, - LargeText: anime.Name, - State: state, - Buttons: []*client.Button{ - { - Label: "View on AniList", - Url: fmt.Sprintf("https://anilist.co/anime/%d", anime.AnilistID), - }, - { - Label: "View on MAL", // Button label - Url: fmt.Sprintf("https://myanimelist.net/anime/%d", anime.MalID), // Button link - }, - }, - } - - // Set the activity - err = client.SetActivity(activity) - if err != nil { - return fmt.Errorf("failed to set Discord activity: %w", err) - } - - if util.IsDebug { - log.Println("Discord Rich Presence updated successfully with cover image link and other details.") - - } - - return nil -} diff --git a/internal/player/discord.go b/internal/player/discord.go new file mode 100644 index 0000000..8006911 --- /dev/null +++ b/internal/player/discord.go @@ -0,0 +1,138 @@ +package player + +import ( + "fmt" + "github.com/alvarorichard/Goanime/internal/api" + "github.com/alvarorichard/Goanime/internal/util" + "github.com/hugolgst/rich-go/client" + "log" + "sync" + "time" +) + +type RichPresenceUpdater struct { + anime *api.Anime + isPaused *bool + animeMutex *sync.Mutex + updateFreq time.Duration + done chan bool + wg sync.WaitGroup + startTime time.Time // Start time of playback + episodeDuration time.Duration // Total duration of the episode + episodeStarted bool // Whether the episode has started + socketPath string // Path to mpv IPC socket +} + +func NewRichPresenceUpdater(anime *api.Anime, isPaused *bool, animeMutex *sync.Mutex, updateFreq time.Duration, episodeDuration time.Duration, socketPath string) *RichPresenceUpdater { + return &RichPresenceUpdater{ + anime: anime, + isPaused: isPaused, + animeMutex: animeMutex, + updateFreq: updateFreq, // Make sure updateFreq is actually used in the struct + done: make(chan bool), + startTime: time.Now(), + episodeDuration: episodeDuration, + episodeStarted: false, + socketPath: socketPath, + } +} + +func (rpu *RichPresenceUpdater) getCurrentPlaybackPosition() (time.Duration, error) { + position, err := mpvSendCommand(rpu.socketPath, []interface{}{"get_property", "time-pos"}) + if err != nil { + return 0, err + } + + // Convert position to float64 and then to time.Duration + posSeconds, ok := position.(float64) + if !ok { + return 0, fmt.Errorf("failed to parse playback position") + } + + return time.Duration(posSeconds) * time.Second, nil +} + +// Start begins the periodic Rich Presence updates. +func (rpu *RichPresenceUpdater) Start() { + rpu.wg.Add(1) + go func() { + defer rpu.wg.Done() + ticker := time.NewTicker(rpu.updateFreq) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + go rpu.updateDiscordPresence() // Run update asynchronously + case <-rpu.done: + if util.IsDebug { + log.Println("Rich Presence updater received stop signal.") + } + return + } + } + }() + if util.IsDebug { + log.Println("Rich Presence updater started.") + } +} + +// Stop signals the updater to stop and waits for the goroutine to finish. +func (rpu *RichPresenceUpdater) Stop() { + close(rpu.done) + rpu.wg.Wait() + if util.IsDebug { + log.Println("Rich Presence updater stopped.") + + } +} + +func (rpu *RichPresenceUpdater) updateDiscordPresence() { + rpu.animeMutex.Lock() + defer rpu.animeMutex.Unlock() + + currentPosition, err := rpu.getCurrentPlaybackPosition() + if err != nil { + if util.IsDebug { + log.Printf("Error fetching playback position: %v\n", err) + } + return + } + + // Debug log to check episode duration + if util.IsDebug { + log.Printf("Episode Duration in updateDiscordPresence: %v seconds (%v minutes)\n", rpu.episodeDuration.Seconds(), rpu.episodeDuration.Minutes()) + + } + + // Convert episode duration to minutes and seconds format + totalMinutes := int(rpu.episodeDuration.Minutes()) + totalSeconds := int(rpu.episodeDuration.Seconds()) % 60 // Remaining seconds after full minutes + + // Format the current playback position as minutes and seconds + timeInfo := fmt.Sprintf("%02d:%02d / %02d:%02d", + int(currentPosition.Minutes()), int(currentPosition.Seconds())%60, + totalMinutes, totalSeconds, + ) + + // Create the activity with updated Details + activity := client.Activity{ + Details: fmt.Sprintf("%s | Episode %s | %s / %d min", rpu.anime.Details.Title.Romaji, rpu.anime.Episodes[0].Number, timeInfo, totalMinutes), + State: "Watching", + LargeImage: rpu.anime.ImageURL, + LargeText: rpu.anime.Details.Title.Romaji, + Buttons: []*client.Button{ + {Label: "View on AniList", Url: fmt.Sprintf("https://anilist.co/anime/%d", rpu.anime.AnilistID)}, + {Label: "View on MAL", Url: fmt.Sprintf("https://myanimelist.net/anime/%d", rpu.anime.MalID)}, + }, + } + + // Set the activity in Discord Rich Presence + if err := client.SetActivity(activity); err != nil { + if util.IsDebug { + log.Printf("Error updating Discord Rich Presence: %v\n", err) + } else { + log.Printf("Discord Rich Presence updated with elapsed time: %s\n", timeInfo) + } + } +} diff --git a/internal/player/helper.go b/internal/player/helper.go new file mode 100644 index 0000000..dec24e1 --- /dev/null +++ b/internal/player/helper.go @@ -0,0 +1,116 @@ +package player + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/progress" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "strings" + "time" +) + +// Update handles updates to the Bubble Tea model. +// +// This function processes incoming messages (`tea.Msg`) and updates the model's state accordingly. +// It locks the model's mutex to ensure thread safety, especially when modifying shared data like +// `m.received`, `m.totalBytes`, and other stateful properties. +// +// The function processes different message types, including: +// +// 1. `tickMsg`: A periodic message that triggers the progress update. If the download is complete +// (`m.done` is `true`), the program quits. Otherwise, it calculates the percentage of bytes received +// and updates the progress bar. It then schedules the next tick. +// +// 2. `statusMsg`: Updates the status string in the model, which can be used to display custom messages +// to the user, such as "Downloading..." or "Download complete". +// +// 3. `progress.FrameMsg`: Handles frame updates for the progress bar. It delegates the update to the +// internal `progress.Model` and returns any commands necessary to refresh the UI. +// +// 4. `tea.KeyMsg`: Responds to key events, such as quitting the program when "Ctrl+C" is pressed. +// If the user requests to quit, the program sets `m.done` to `true` and returns the quit command. +// +// For unhandled message types, it returns the model unchanged. +// +// Returns: +// - Updated `tea.Model` representing the current state of the model. +// - A `tea.Cmd` that specifies the next action the program should perform. +func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + m.mu.Lock() + defer m.mu.Unlock() + + switch msg := msg.(type) { + case tickMsg: + if m.done { + return m, tea.Quit + } + if m.totalBytes > 0 { + cmd := m.progress.SetPercent(float64(m.received) / float64(m.totalBytes)) + return m, tea.Batch(cmd, tickCmd()) + } + return m, tickCmd() + + case statusMsg: + m.status = string(msg) + return m, nil + + case progress.FrameMsg: + var cmd tea.Cmd + var newModel tea.Model + newModel, cmd = m.progress.Update(msg) + m.progress = newModel.(progress.Model) + return m, cmd + + case tea.KeyMsg: + if key.Matches(msg, m.keys.quit) { + m.done = true + return m, tea.Quit + } + return m, nil + + default: + return m, nil + } +} + +// View renders the Bubble Tea model +// View renders the user interface for the Bubble Tea model. +// +// This function generates the visual output that is displayed to the user. It includes the status message, +// the progress bar, and a quit instruction. The layout is formatted with padding for proper alignment. +// +// Steps: +// 1. Adds padding to each line using spaces. +// 2. Styles the status message (m.status) with an orange color (#FFA500). +// 3. Displays the progress bar using the progress model. +// 4. Shows a message instructing the user to press "Ctrl+C" to quit. +// +// Returns: +// - A formatted string that represents the UI for the current state of the model. +func (m *model) View() string { + // Creates padding spaces for consistent layout + pad := strings.Repeat(" ", padding) + + // Styles the status message with an orange color + statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500")) + + // Returns the UI layout: status message, progress bar, and quit instruction + return "\n" + + pad + statusStyle.Render(m.status) + "\n\n" + // Render the styled status message + pad + m.progress.View() + "\n\n" + // Render the progress bar + pad + "Press Ctrl+C to quit" // Show quit instruction +} + +// tickCmd returns a command that triggers a "tick" every 100 milliseconds. +// +// This function sets up a recurring event (tick) that fires every 100 milliseconds. +// Each tick sends a `tickMsg` with the current time (`t`) as a message, which can be +// handled by the update function to trigger actions like updating the progress bar. +// +// Returns: +// - A `tea.Cmd` that schedules a tick every 100 milliseconds and sends a `tickMsg`. +func tickCmd() tea.Cmd { + return tea.Tick(time.Millisecond*100, func(t time.Time) tea.Msg { + return tickMsg(t) + }) +} diff --git a/internal/player/player.go b/internal/player/player.go index ae557f2..b82b2fd 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -5,7 +5,6 @@ import ( "crypto/rand" "encoding/json" "fmt" - "github.com/hugolgst/rich-go/client" "io" "log" "net" @@ -14,21 +13,17 @@ import ( "os/exec" "os/user" "path/filepath" - "regexp" "runtime" "strconv" "strings" "sync" "time" - "github.com/PuerkitoBio/goquery" "github.com/alvarorichard/Goanime/internal/api" "github.com/alvarorichard/Goanime/internal/util" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/progress" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/ktr0731/go-fuzzyfinder" "github.com/manifoldco/promptui" "github.com/pkg/errors" ) @@ -63,133 +58,6 @@ func (m *model) Init() tea.Cmd { return tea.Batch(tickCmd(), m.progress.Init()) } -type RichPresenceUpdater struct { - anime *api.Anime - isPaused *bool - animeMutex *sync.Mutex - updateFreq time.Duration - done chan bool - wg sync.WaitGroup - startTime time.Time // Start time of playback - episodeDuration time.Duration // Total duration of the episode - episodeStarted bool // Whether the episode has started - socketPath string // Path to mpv IPC socket -} - -func NewRichPresenceUpdater(anime *api.Anime, isPaused *bool, animeMutex *sync.Mutex, updateFreq time.Duration, episodeDuration time.Duration, socketPath string) *RichPresenceUpdater { - return &RichPresenceUpdater{ - anime: anime, - isPaused: isPaused, - animeMutex: animeMutex, - updateFreq: updateFreq, // Make sure updateFreq is actually used in the struct - done: make(chan bool), - startTime: time.Now(), - episodeDuration: episodeDuration, - episodeStarted: false, - socketPath: socketPath, - } -} - -func (rpu *RichPresenceUpdater) getCurrentPlaybackPosition() (time.Duration, error) { - position, err := mpvSendCommand(rpu.socketPath, []interface{}{"get_property", "time-pos"}) - if err != nil { - return 0, err - } - - // Convert position to float64 and then to time.Duration - posSeconds, ok := position.(float64) - if !ok { - return 0, fmt.Errorf("failed to parse playback position") - } - - return time.Duration(posSeconds) * time.Second, nil -} - -// Start begins the periodic Rich Presence updates. -func (rpu *RichPresenceUpdater) Start() { - rpu.wg.Add(1) - go func() { - defer rpu.wg.Done() - ticker := time.NewTicker(rpu.updateFreq) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - go rpu.updateDiscordPresence() // Run update asynchronously - case <-rpu.done: - if util.IsDebug { - log.Println("Rich Presence updater received stop signal.") - } - return - } - } - }() - if util.IsDebug { - log.Println("Rich Presence updater started.") - } -} - -// Stop signals the updater to stop and waits for the goroutine to finish. -func (rpu *RichPresenceUpdater) Stop() { - close(rpu.done) - rpu.wg.Wait() - if util.IsDebug { - log.Println("Rich Presence updater stopped.") - - } -} - -func (rpu *RichPresenceUpdater) updateDiscordPresence() { - rpu.animeMutex.Lock() - defer rpu.animeMutex.Unlock() - - currentPosition, err := rpu.getCurrentPlaybackPosition() - if err != nil { - if util.IsDebug { - log.Printf("Error fetching playback position: %v\n", err) - } - return - } - - // Debug log to check episode duration - if util.IsDebug { - log.Printf("Episode Duration in updateDiscordPresence: %v seconds (%v minutes)\n", rpu.episodeDuration.Seconds(), rpu.episodeDuration.Minutes()) - - } - - // Convert episode duration to minutes and seconds format - totalMinutes := int(rpu.episodeDuration.Minutes()) - totalSeconds := int(rpu.episodeDuration.Seconds()) % 60 // Remaining seconds after full minutes - - // Format the current playback position as minutes and seconds - timeInfo := fmt.Sprintf("%02d:%02d / %02d:%02d", - int(currentPosition.Minutes()), int(currentPosition.Seconds())%60, - totalMinutes, totalSeconds, - ) - - // Create the activity with updated Details - activity := client.Activity{ - Details: fmt.Sprintf("%s | Episode %s | %s / %d min", rpu.anime.Details.Title.Romaji, rpu.anime.Episodes[0].Number, timeInfo, totalMinutes), - State: "Watching", - LargeImage: rpu.anime.ImageURL, - LargeText: rpu.anime.Details.Title.Romaji, - Buttons: []*client.Button{ - {Label: "View on AniList", Url: fmt.Sprintf("https://anilist.co/anime/%d", rpu.anime.AnilistID)}, - {Label: "View on MAL", Url: fmt.Sprintf("https://myanimelist.net/anime/%d", rpu.anime.MalID)}, - }, - } - - // Set the activity in Discord Rich Presence - if err := client.SetActivity(activity); err != nil { - if util.IsDebug { - log.Printf("Error updating Discord Rich Presence: %v\n", err) - } else { - log.Printf("Discord Rich Presence updated with elapsed time: %s\n", timeInfo) - } - } -} - // StartVideo opens mpv with a socket for IPC func StartVideo(link string, args []string) (string, error) { randomBytes := make([]byte, 4) @@ -217,7 +85,6 @@ func StartVideo(link string, args []string) (string, error) { } // mpvSendCommand sends a JSON command to MPV via the IPC socket and receives the response. -// mpvSendCommand sends a JSON command to mpv via a socket and reads the response. func mpvSendCommand(socketPath string, command []interface{}) (interface{}, error) { conn, err := dialMPVSocket(socketPath) if err != nil { @@ -271,224 +138,6 @@ func dialMPVSocket(socketPath string) (net.Conn, error) { } } -// WINDOWS RELEASE - -//func dialMPVSocket(socketPath string) (net.Conn, error) { -// if runtime.GOOS == "windows" { -// //Attempt to connect using named pipe on Windows -// conn, err := winio.DialPipe(socketPath, nil) -// if err != nil { -// return nil, fmt.Errorf("failed to connect to named pipe: %w", err) -// } -// return conn, nil -// } else { -// // Unix-like system uses Unix sockets -// conn, err := net.Dial("unix", socketPath) -// if err != nil { -// return nil, fmt.Errorf("failed to connect to Unix socket: %w", err) -// } -// return conn, nil -// } -//} - -// Update handles updates to the Bubble Tea model. -// -// This function processes incoming messages (`tea.Msg`) and updates the model's state accordingly. -// It locks the model's mutex to ensure thread safety, especially when modifying shared data like -// `m.received`, `m.totalBytes`, and other stateful properties. -// -// The function processes different message types, including: -// -// 1. `tickMsg`: A periodic message that triggers the progress update. If the download is complete -// (`m.done` is `true`), the program quits. Otherwise, it calculates the percentage of bytes received -// and updates the progress bar. It then schedules the next tick. -// -// 2. `statusMsg`: Updates the status string in the model, which can be used to display custom messages -// to the user, such as "Downloading..." or "Download complete". -// -// 3. `progress.FrameMsg`: Handles frame updates for the progress bar. It delegates the update to the -// internal `progress.Model` and returns any commands necessary to refresh the UI. -// -// 4. `tea.KeyMsg`: Responds to key events, such as quitting the program when "Ctrl+C" is pressed. -// If the user requests to quit, the program sets `m.done` to `true` and returns the quit command. -// -// For unhandled message types, it returns the model unchanged. -// -// Returns: -// - Updated `tea.Model` representing the current state of the model. -// - A `tea.Cmd` that specifies the next action the program should perform. - -func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - m.mu.Lock() - defer m.mu.Unlock() - - switch msg := msg.(type) { - case tickMsg: - if m.done { - return m, tea.Quit - } - if m.totalBytes > 0 { - cmd := m.progress.SetPercent(float64(m.received) / float64(m.totalBytes)) - return m, tea.Batch(cmd, tickCmd()) - } - return m, tickCmd() - - case statusMsg: - m.status = string(msg) - return m, nil - - case progress.FrameMsg: - var cmd tea.Cmd - var newModel tea.Model - newModel, cmd = m.progress.Update(msg) - m.progress = newModel.(progress.Model) - return m, cmd - - case tea.KeyMsg: - if key.Matches(msg, m.keys.quit) { - m.done = true - return m, tea.Quit - } - return m, nil - - default: - return m, nil - } -} - -// View renders the Bubble Tea model -// View renders the user interface for the Bubble Tea model. -// -// This function generates the visual output that is displayed to the user. It includes the status message, -// the progress bar, and a quit instruction. The layout is formatted with padding for proper alignment. -// -// Steps: -// 1. Adds padding to each line using spaces. -// 2. Styles the status message (m.status) with an orange color (#FFA500). -// 3. Displays the progress bar using the progress model. -// 4. Shows a message instructing the user to press "Ctrl+C" to quit. -// -// Returns: -// - A formatted string that represents the UI for the current state of the model. -func (m *model) View() string { - // Creates padding spaces for consistent layout - pad := strings.Repeat(" ", padding) - - // Styles the status message with an orange color - statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500")) - - // Returns the UI layout: status message, progress bar, and quit instruction - return "\n" + - pad + statusStyle.Render(m.status) + "\n\n" + // Render the styled status message - pad + m.progress.View() + "\n\n" + // Render the progress bar - pad + "Press Ctrl+C to quit" // Show quit instruction -} - -// tickCmd returns a command that triggers a "tick" every 100 milliseconds. -// -// This function sets up a recurring event (tick) that fires every 100 milliseconds. -// Each tick sends a `tickMsg` with the current time (`t`) as a message, which can be -// handled by the update function to trigger actions like updating the progress bar. -// -// Returns: -// - A `tea.Cmd` that schedules a tick every 100 milliseconds and sends a `tickMsg`. -func tickCmd() tea.Cmd { - return tea.Tick(time.Millisecond*100, func(t time.Time) tea.Msg { - return tickMsg(t) - }) -} - -// statusUpdateCmd returns a command to update the status - -// DownloadFolderFormatter formats the anime URL to create a download folder name. -// -// This function extracts a specific part of the anime video URL to use it as the name -// for the download folder. It uses a regular expression to capture the part of the URL -// after "/video/", which is often unique and suitable as a folder name. -// -// Steps: -// 1. Compiles a regular expression that matches URLs of the form "https:///video/". -// 2. Extracts the "" from the URL. -// 3. If the match is successful, it returns the extracted part as the folder name. -// 4. If no match is found, it returns an empty string. -// -// Parameters: -// - str: The anime video URL as a string. -// -// Returns: -// - A string representing the formatted folder name, or an empty string if no match is found. -func DownloadFolderFormatter(str string) string { - // Regular expression to capture the unique part after "/video/" - regex := regexp.MustCompile(`https?://[^/]+/video/([^/?]+)`) - - // Apply the regex to the input URL - match := regex.FindStringSubmatch(str) - - // If a match is found, return the captured group (folder name) - if len(match) > 1 { - finalStep := match[1] - return finalStep - } - - // If no match, return an empty string - return "" -} - -// getContentLength retrieves the content length of the given URL. -func getContentLength(url string, client *http.Client) (int64, error) { - // Attempts to create an HTTP HEAD request to retrieve headers without downloading the body. - req, err := http.NewRequest("HEAD", url, nil) - if err != nil { - // Returns 0 and the error if the request creation fails. - return 0, err - } - - // Sends the HEAD request to the server. - resp, err := client.Do(req) - if err != nil || resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotImplemented { - // If the HEAD request fails or is not supported, fall back to a GET request. - req.Method = "GET" - req.Header.Set("Range", "bytes=0-0") // Requests only the first byte to minimize data transfer. - resp, err = client.Do(req) // Sends the modified GET request. - if err != nil { - // Returns 0 and the error if the GET request fails. - return 0, err - } - } - - // Ensures that the response body is closed after it is used to avoid resource leaks. - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - // Logs a warning if closing the response body fails. - log.Printf("Failed to close response body: %v\n", err) - } - }(resp.Body) - - // Checks if the server responded with a 200 OK or 206 Partial Content status. - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { - // Returns an error if the server does not support partial content (required for ranged requests). - return 0, fmt.Errorf("server does not support partial content: status code %d", resp.StatusCode) - } - - // Retrieves the "Content-Length" header from the response. - contentLengthHeader := resp.Header.Get("Content-Length") - if contentLengthHeader == "" { - // Returns an error if the "Content-Length" header is missing. - return 0, fmt.Errorf("Content-Length header is missing") - } - - // Converts the "Content-Length" header from a string to an int64. - contentLength, err := strconv.ParseInt(contentLengthHeader, 10, 64) - if err != nil { - // Returns 0 and an error if the conversion fails. - return 0, err - } - - // Returns the content length in bytes. - return contentLength, nil -} - // downloadPart downloads a part of the video file. // // This function downloads a specific part (or chunk) of a video file using HTTP ranged requests. @@ -676,7 +325,7 @@ func DownloadVideo(url, destPath string, numThreads int, m *model) error { // Cleans the destination path to ensure it is valid and well-formed. destPath = filepath.Clean(destPath) - // Creates an HTTP client with a custom transport that includes a 10-second timeout. + // Creates an HTTP client with custom transport that includes a 10-second timeout. httpClient := &http.Client{ Transport: api.SafeTransport(10 * time.Second), } @@ -761,8 +410,6 @@ func DownloadVideo(url, destPath string, numThreads int, m *model) error { // } //} -// HandleDownloadAndPlay handles the download and playback of the video - // HandleDownloadAndPlay handles the download and playback of the video func HandleDownloadAndPlay( videoURL string, @@ -805,89 +452,6 @@ func HandleDownloadAndPlay( } } -//func downloadAndPlayEpisode(videoURL string, episodes []api.Episode, selectedEpisodeNum int, animeURL, episodeNumberStr string, updater *RichPresenceUpdater) { -// currentUser, err := user.Current() -// if err != nil { -// log.Panicln("Failed to get current user:", util.ErrorHandler(err)) -// } -// -// downloadPath := filepath.Join(currentUser.HomeDir, ".local", "goanime", "downloads", "anime", DownloadFolderFormatter(animeURL)) -// episodePath := filepath.Join(downloadPath, episodeNumberStr+".mp4") -// -// if _, err := os.Stat(downloadPath); os.IsNotExist(err) { -// if err := os.MkdirAll(downloadPath, os.ModePerm); err != nil { -// log.Panicln("Failed to create download directory:", util.ErrorHandler(err)) -// } -// } -// -// if _, err := os.Stat(episodePath); os.IsNotExist(err) { -// numThreads := 4 // Define the number of threads for downloading -// -// // Check if the video URL is from Blogger -// if strings.Contains(videoURL, "blogger.com") { -// // Use yt-dlp to download the video from Blogger -// fmt.Printf("Downloading episode %s with yt-dlp...\n", episodeNumberStr) -// cmd := exec.Command("yt-dlp", "--no-progress", "-o", episodePath, videoURL) -// if err := cmd.Run(); err != nil { -// log.Panicln("Failed to download video using yt-dlp:", util.ErrorHandler(err)) -// } -// fmt.Printf("Download of episode %s completed!\n", episodeNumberStr) -// } else { -// // Initialize progress model -// m := &model{ -// progress: progress.New(progress.WithDefaultGradient()), -// keys: keyMap{ -// quit: key.NewBinding( -// key.WithKeys("ctrl+c"), -// key.WithHelp("ctrl+c", "quit"), -// ), -// }, -// } -// p := tea.NewProgram(m) -// -// // Get content length -// httpClient := &http.Client{ -// Transport: api.SafeTransport(10 * time.Second), -// } -// contentLength, err := getContentLength(videoURL, httpClient) -// if err != nil { -// log.Panicln("Failed to get content length:", util.ErrorHandler(err)) -// } -// m.totalBytes = contentLength -// -// // Start the download in a separate goroutine -// go func() { -// // Update status -// p.Send(statusMsg(fmt.Sprintf("Downloading episode %s...", episodeNumberStr))) -// -// if err := DownloadVideo(videoURL, episodePath, numThreads, m); err != nil { -// log.Panicln("Failed to download video:", util.ErrorHandler(err)) -// } -// -// m.mu.Lock() -// m.done = true -// m.mu.Unlock() -// -// // Final status update -// p.Send(statusMsg("Download completed!")) -// }() -// -// // Run the Bubble Tea program in the main goroutine -// if _, err := p.Run(); err != nil { -// log.Fatalf("error running progress bar: %v", err) -// } -// } -// } else { -// fmt.Println("Video already downloaded.") -// } -// -// if askForPlayOffline() { -// if err := playVideo(episodePath, episodes, selectedEpisodeNum, updater); err != nil { -// log.Panicln("Failed to play video:", util.ErrorHandler(err)) -// } -// } -//} - func downloadAndPlayEpisode( videoURL string, episodes []api.Episode, @@ -1328,198 +892,6 @@ func HandleBatchDownload(episodes []api.Episode, animeURL string) error { return nil } -// SelectEpisodeWithFuzzyFinder allows the user to select an episode using fuzzy finder -func SelectEpisodeWithFuzzyFinder(episodes []api.Episode) (string, string, error) { - if len(episodes) == 0 { - return "", "", errors.New("no episodes provided") - } - - idx, err := fuzzyfinder.Find( - episodes, - func(i int) string { - return episodes[i].Number - }, - fuzzyfinder.WithPromptString("Select the episode"), - ) - if err != nil { - return "", "", fmt.Errorf("failed to select episode with go-fuzzyfinder: %w", err) - } - - if idx < 0 || idx >= len(episodes) { - return "", "", errors.New("invalid index returned by fuzzyfinder") - } - - return episodes[idx].URL, episodes[idx].Number, nil -} - -// ExtractEpisodeNumber extracts the numeric part of an episode string -func ExtractEpisodeNumber(episodeStr string) string { - numRe := regexp.MustCompile(`\d+`) - numStr := numRe.FindString(episodeStr) - if numStr == "" { - return "1" - } - return numStr -} - -// GetVideoURLForEpisode gets the video URL for a given episode URL -func GetVideoURLForEpisode(episodeURL string) (string, error) { - - if util.IsDebug { - log.Printf("Tentando extrair URL de vídeo para o episódio: %s", episodeURL) - } - videoURL, err := extractVideoURL(episodeURL) - if err != nil { - return "", err - } - return extractActualVideoURL(videoURL) -} - -func extractVideoURL(url string) (string, error) { - - if util.IsDebug { - log.Printf("Extraindo URL de vídeo da página: %s", url) - } - - response, err := api.SafeGet(url) - if err != nil { - return "", errors.New(fmt.Sprintf("failed to fetch URL: %+v", err)) - } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - log.Printf("Failed to close response body: %v\n", err) - } - }(response.Body) - - doc, err := goquery.NewDocumentFromReader(response.Body) - if err != nil { - return "", errors.New(fmt.Sprintf("failed to parse HTML: %+v", err)) - } - - videoElements := doc.Find("video") - if videoElements.Length() == 0 { - videoElements = doc.Find("div") - } - - if videoElements.Length() == 0 { - return "", errors.New("no video elements found in the HTML") - } - - videoSrc, exists := videoElements.Attr("data-video-src") - if !exists || videoSrc == "" { - urlBody, err := fetchContent(url) - if err != nil { - return "", err - } - videoSrc, err = findBloggerLink(urlBody) - if err != nil { - return "", err - } - } - - return videoSrc, nil -} - -func fetchContent(url string) (string, error) { - resp, err := api.SafeGet(url) - if err != nil { - return "", err - } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - log.Printf("Failed to close response body: %v\n", err) - } - }(resp.Body) - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - return string(body), nil -} - -func findBloggerLink(content string) (string, error) { - pattern := `https://www\.blogger\.com/video\.g\?token=([A-Za-z0-9_-]+)` - - re := regexp.MustCompile(pattern) - matches := re.FindStringSubmatch(content) - - if len(matches) > 0 { - return matches[0], nil - } else { - return "", errors.New("no blogger video link found in the content") - } -} - -func extractActualVideoURL(videoSrc string) (string, error) { - if strings.Contains(videoSrc, "blogger.com") { - return videoSrc, nil - } - response, err := api.SafeGet(videoSrc) - if err != nil { - return "", errors.New(fmt.Sprintf("failed to fetch video source: %+v", err)) - } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - log.Printf("Failed to close response body: %v\n", err) - } - }(response.Body) - - if response.StatusCode != http.StatusOK { - return "", errors.New(fmt.Sprintf("request failed with status: %s", response.Status)) - } - - body, err := io.ReadAll(response.Body) - if err != nil { - return "", errors.New(fmt.Sprintf("failed to read response body: %+v", err)) - } - - var videoResponse VideoResponse - if err := json.Unmarshal(body, &videoResponse); err != nil { - return "", errors.New(fmt.Sprintf("failed to unmarshal JSON response: %+v", err)) - } - - if len(videoResponse.Data) == 0 { - return "", errors.New("no video data found in the response") - } - - highestQualityVideoURL := selectHighestQualityVideo(videoResponse.Data) - if highestQualityVideoURL == "" { - return "", errors.New("no suitable video quality found") - } - - return highestQualityVideoURL, nil -} - -// VideoData represents the video data structure, with a source URL and a label -type VideoData struct { - Src string `json:"src"` - Label string `json:"label"` -} - -// VideoResponse represents the video response structure with a slice of VideoData -type VideoResponse struct { - Data []VideoData `json:"data"` -} - -// selectHighestQualityVideo selects the highest quality video available -func selectHighestQualityVideo(videos []VideoData) string { - var highestQuality int - var highestQualityURL string - for _, video := range videos { - qualityValue, _ := strconv.Atoi(strings.TrimRight(video.Label, "p")) - if qualityValue > highestQuality { - highestQuality = qualityValue - highestQualityURL = video.Src - } - } - return highestQualityURL -} - // playVideo handles the online playback of a video and user interaction. func playVideo( videoURL string, diff --git a/internal/player/scraper.go b/internal/player/scraper.go new file mode 100644 index 0000000..76800cb --- /dev/null +++ b/internal/player/scraper.go @@ -0,0 +1,318 @@ +package player + +import ( + "encoding/json" + "fmt" + "github.com/PuerkitoBio/goquery" + "github.com/alvarorichard/Goanime/internal/api" + "github.com/alvarorichard/Goanime/internal/util" + "github.com/ktr0731/go-fuzzyfinder" + "github.com/pkg/errors" + "io" + "log" + "net/http" + "regexp" + "strconv" + "strings" +) + +// WINDOWS RELEASE + +//func dialMPVSocket(socketPath string) (net.Conn, error) { +// if runtime.GOOS == "windows" { +// //Attempt to connect using named pipe on Windows +// conn, err := winio.DialPipe(socketPath, nil) +// if err != nil { +// return nil, fmt.Errorf("failed to connect to named pipe: %w", err) +// } +// return conn, nil +// } else { +// // Unix-like system uses Unix sockets +// conn, err := net.Dial("unix", socketPath) +// if err != nil { +// return nil, fmt.Errorf("failed to connect to Unix socket: %w", err) +// } +// return conn, nil +// } +//} + +// DownloadFolderFormatter formats the anime URL to create a download folder name. +// +// This function extracts a specific part of the anime video URL to use it as the name +// for the download folder. It uses a regular expression to capture the part of the URL +// after "/video/", which is often unique and suitable as a folder name. +// +// Steps: +// 1. Compiles a regular expression that matches URLs of the form "https:///video/". +// 2. Extracts the "" from the URL. +// 3. If the match is successful, it returns the extracted part as the folder name. +// 4. If no match is found, it returns an empty string. +// +// Parameters: +// - str: The anime video URL as a string. +// +// Returns: +// - A string representing the formatted folder name, or an empty string if no match is found. +func DownloadFolderFormatter(str string) string { + // Regular expression to capture the unique part after "/video/" + regex := regexp.MustCompile(`https?://[^/]+/video/([^/?]+)`) + + // Apply the regex to the input URL + match := regex.FindStringSubmatch(str) + + // If a match is found, return the captured group (folder name) + if len(match) > 1 { + finalStep := match[1] + return finalStep + } + + // If no match, return an empty string + return "" +} + +// getContentLength retrieves the content length of the given URL. +func getContentLength(url string, client *http.Client) (int64, error) { + // Attempts to create an HTTP HEAD request to retrieve headers without downloading the body. + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + // Returns 0 and the error if the request creation fails. + return 0, err + } + + // Sends the HEAD request to the server. + resp, err := client.Do(req) + if err != nil || resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotImplemented { + // If the HEAD request fails or is not supported, fall back to a GET request. + req.Method = "GET" + req.Header.Set("Range", "bytes=0-0") // Requests only the first byte to minimize data transfer. + resp, err = client.Do(req) // Sends the modified GET request. + if err != nil { + // Returns 0 and the error if the GET request fails. + return 0, err + } + } + + // Ensures that the response body is closed after it is used to avoid resource leaks. + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + // Logs a warning if closing the response body fails. + log.Printf("Failed to close response body: %v\n", err) + } + }(resp.Body) + + // Checks if the server responded with a 200 OK or 206 Partial Content status. + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { + // Returns an error if the server does not support partial content (required for ranged requests). + return 0, fmt.Errorf("server does not support partial content: status code %d", resp.StatusCode) + } + + // Retrieves the "Content-Length" header from the response. + contentLengthHeader := resp.Header.Get("Content-Length") + if contentLengthHeader == "" { + // Returns an error if the "Content-Length" header is missing. + return 0, fmt.Errorf("Content-Length header is missing") + } + + // Converts the "Content-Length" header from a string to an int64. + contentLength, err := strconv.ParseInt(contentLengthHeader, 10, 64) + if err != nil { + // Returns 0 and an error if the conversion fails. + return 0, err + } + + // Returns the content length in bytes. + return contentLength, nil +} + +// SelectEpisodeWithFuzzyFinder allows the user to select an episode using fuzzy finder +func SelectEpisodeWithFuzzyFinder(episodes []api.Episode) (string, string, error) { + if len(episodes) == 0 { + return "", "", errors.New("no episodes provided") + } + + idx, err := fuzzyfinder.Find( + episodes, + func(i int) string { + return episodes[i].Number + }, + fuzzyfinder.WithPromptString("Select the episode"), + ) + if err != nil { + return "", "", fmt.Errorf("failed to select episode with go-fuzzyfinder: %w", err) + } + + if idx < 0 || idx >= len(episodes) { + return "", "", errors.New("invalid index returned by fuzzyfinder") + } + + return episodes[idx].URL, episodes[idx].Number, nil +} + +// ExtractEpisodeNumber extracts the numeric part of an episode string +func ExtractEpisodeNumber(episodeStr string) string { + numRe := regexp.MustCompile(`\d+`) + numStr := numRe.FindString(episodeStr) + if numStr == "" { + return "1" + } + return numStr +} + +// GetVideoURLForEpisode gets the video URL for a given episode URL +func GetVideoURLForEpisode(episodeURL string) (string, error) { + + if util.IsDebug { + log.Printf("Tentando extrair URL de vídeo para o episódio: %s", episodeURL) + } + videoURL, err := extractVideoURL(episodeURL) + if err != nil { + return "", err + } + return extractActualVideoURL(videoURL) +} + +func extractVideoURL(url string) (string, error) { + + if util.IsDebug { + log.Printf("Extraindo URL de vídeo da página: %s", url) + } + + response, err := api.SafeGet(url) + if err != nil { + return "", errors.New(fmt.Sprintf("failed to fetch URL: %+v", err)) + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Printf("Failed to close response body: %v\n", err) + } + }(response.Body) + + doc, err := goquery.NewDocumentFromReader(response.Body) + if err != nil { + return "", errors.New(fmt.Sprintf("failed to parse HTML: %+v", err)) + } + + videoElements := doc.Find("video") + if videoElements.Length() == 0 { + videoElements = doc.Find("div") + } + + if videoElements.Length() == 0 { + return "", errors.New("no video elements found in the HTML") + } + + videoSrc, exists := videoElements.Attr("data-video-src") + if !exists || videoSrc == "" { + urlBody, err := fetchContent(url) + if err != nil { + return "", err + } + videoSrc, err = findBloggerLink(urlBody) + if err != nil { + return "", err + } + } + + return videoSrc, nil +} + +func fetchContent(url string) (string, error) { + resp, err := api.SafeGet(url) + if err != nil { + return "", err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Printf("Failed to close response body: %v\n", err) + } + }(resp.Body) + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(body), nil +} + +func findBloggerLink(content string) (string, error) { + pattern := `https://www\.blogger\.com/video\.g\?token=([A-Za-z0-9_-]+)` + + re := regexp.MustCompile(pattern) + matches := re.FindStringSubmatch(content) + + if len(matches) > 0 { + return matches[0], nil + } else { + return "", errors.New("no blogger video link found in the content") + } +} + +func extractActualVideoURL(videoSrc string) (string, error) { + if strings.Contains(videoSrc, "blogger.com") { + return videoSrc, nil + } + response, err := api.SafeGet(videoSrc) + if err != nil { + return "", errors.New(fmt.Sprintf("failed to fetch video source: %+v", err)) + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Printf("Failed to close response body: %v\n", err) + } + }(response.Body) + + if response.StatusCode != http.StatusOK { + return "", errors.New(fmt.Sprintf("request failed with status: %s", response.Status)) + } + + body, err := io.ReadAll(response.Body) + if err != nil { + return "", errors.New(fmt.Sprintf("failed to read response body: %+v", err)) + } + + var videoResponse VideoResponse + if err := json.Unmarshal(body, &videoResponse); err != nil { + return "", errors.New(fmt.Sprintf("failed to unmarshal JSON response: %+v", err)) + } + + if len(videoResponse.Data) == 0 { + return "", errors.New("no video data found in the response") + } + + highestQualityVideoURL := selectHighestQualityVideo(videoResponse.Data) + if highestQualityVideoURL == "" { + return "", errors.New("no suitable video quality found") + } + + return highestQualityVideoURL, nil +} + +// VideoData represents the video data structure, with a source URL and a label +type VideoData struct { + Src string `json:"src"` + Label string `json:"label"` +} + +// VideoResponse represents the video response structure with a slice of VideoData +type VideoResponse struct { + Data []VideoData `json:"data"` +} + +// selectHighestQualityVideo selects the highest quality video available +func selectHighestQualityVideo(videos []VideoData) string { + var highestQuality int + var highestQualityURL string + for _, video := range videos { + qualityValue, _ := strconv.Atoi(strings.TrimRight(video.Label, "p")) + if qualityValue > highestQuality { + highestQuality = qualityValue + highestQualityURL = video.Src + } + } + return highestQualityURL +} From 4747359db18fba9dcd1994b311bdefff58a4aa97 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Fri, 3 Jan 2025 21:07:27 -0300 Subject: [PATCH 4/4] fixed CamelCase typo --- internal/player/discord.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/player/discord.go b/internal/player/discord.go index 8006911..4a98aac 100644 --- a/internal/player/discord.go +++ b/internal/player/discord.go @@ -37,7 +37,7 @@ func NewRichPresenceUpdater(anime *api.Anime, isPaused *bool, animeMutex *sync.M } } -func (rpu *RichPresenceUpdater) getCurrentPlaybackPosition() (time.Duration, error) { +func (rpu *RichPresenceUpdater) GetCurrentPlaybackPosition() (time.Duration, error) { position, err := mpvSendCommand(rpu.socketPath, []interface{}{"get_property", "time-pos"}) if err != nil { return 0, err @@ -91,7 +91,7 @@ func (rpu *RichPresenceUpdater) updateDiscordPresence() { rpu.animeMutex.Lock() defer rpu.animeMutex.Unlock() - currentPosition, err := rpu.getCurrentPlaybackPosition() + currentPosition, err := rpu.GetCurrentPlaybackPosition() if err != nil { if util.IsDebug { log.Printf("Error fetching playback position: %v\n", err)